diff options
330 files changed, 7464 insertions, 5389 deletions
diff --git a/.github/commands.json b/.github/commands.json index 68a50baae95..f67ee72c873 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -154,7 +154,7 @@ "IllusionMH" ], "action": "updateLabels", - "addLabel": "~needs more info" + "addLabel": "~info-needed" }, { "type": "comment", @@ -165,14 +165,14 @@ "gjsjohnmurray", "IllusionMH" ], - "addLabel": "needs more info", + "addLabel": "info-needed", "comment": "Thanks for creating this issue regarding performance! Please follow this guide to help us diagnose performance issues: https://github.com/microsoft/vscode/wiki/Performance-Issues \n\nHappy Coding!" }, { "type": "comment", "name": "jsDebugLogs", "action": "updateLabels", - "addLabel": "needs more info", + "addLabel": "info-needed", "comment": "Please collect trace logs using the following instructions:\n\n> If you're able to, add `\"trace\": true` to your `launch.json` and reproduce the issue. The location of the log file on your disk will be written to the Debug Console. Share that with us.\n>\n> ⚠️ This log file will not contain source code, but will contain file paths. You can drop it into https://microsoft.github.io/vscode-pwa-analyzer/index.html to see what it contains. If you'd rather not share the log publicly, you can email it to connor@xbox.com" }, { @@ -189,17 +189,17 @@ }, { "type": "label", - "name": "~needs more info", + "name": "~info-needed", "action": "updateLabels", - "addLabel": "needs more info", - "removeLabel": "~needs more info", + "addLabel": "info-needed", + "removeLabel": "~info-needed", "comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" }, { "type": "label", "name": "~needs version info", "action": "updateLabels", - "addLabel": "needs more info", + "addLabel": "info-needed", "removeLabel": "~needs version info", "comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" }, @@ -416,7 +416,7 @@ "IllusionMH" ], "action": "comment", - "addLabel": "needs more info", + "addLabel": "info-needed", "comment": "Thanks for reporting this issue! Unfortunately, it's hard for us to understand what issue you're seeing. Please help us out by providing a screen recording showing exactly what isn't working as expected. While we can work with most standard formats, `.gif` files are preferred as they are displayed inline on GitHub. You may find https://gifcap.dev helpful as a browser-based gif recording tool.\n\nIf the issue depends on keyboard input, you can help us by enabling screencast mode for the recording (`Developer: Toggle Screencast Mode` in the command palette).\n\nHappy coding!" }, { diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 19314029215..5335e645320 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,5 +5,3 @@ * Ensure that the code is up-to-date with the `main` branch. * Include a description of the proposed changes and how to test them. --> - -This PR fixes # diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000000..0cf077e6b4c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16.14 diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml index 4c30c5e0b31..1094b41ca21 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -1,4 +1,6 @@ parameters: + - name: VSCODE_QUALITY + type: string - name: VSCODE_RUN_UNIT_TESTS type: boolean - name: VSCODE_RUN_INTEGRATION_TESTS @@ -14,25 +16,43 @@ steps: displayName: Download Electron and Playwright - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - - script: | - set -e - ./scripts/test.sh --build --tfs "Unit Tests" - displayName: Run unit tests (Electron) - timeoutInMinutes: 15 - - - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - - script: | - set -e - yarn test-node --build - displayName: Run unit tests (node.js) - timeoutInMinutes: 15 - - - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - - script: | - set -e - DEBUG=*browser* yarn test-browser-no-install --sequential --build --browser chromium --browser webkit --tfs "Browser Unit Tests" - displayName: Run unit tests (Browser, Chromium & Webkit) - timeoutInMinutes: 30 + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + ./scripts/test.sh --tfs "Unit Tests" + displayName: Run unit tests (Electron) + timeoutInMinutes: 15 + + - script: | + set -e + yarn test-node + displayName: Run unit tests (node.js) + timeoutInMinutes: 15 + + - script: | + set -e + DEBUG=*browser* yarn test-browser-no-install --sequential --browser chromium --browser webkit --tfs "Browser Unit Tests" + displayName: Run unit tests (Browser, Chromium & Webkit) + timeoutInMinutes: 30 + + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - 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 - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - script: | @@ -57,38 +77,42 @@ steps: compile-extension:vscode-test-resolver displayName: Build integration tests - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - script: | - # Figure out the full absolute path of the product we just built - # including the remote server and configure the integration tests - # to run with these builds instead of running out of sources. - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - APP_NAME="`ls $APP_ROOT | head -n 1`" - INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ - ./scripts/test-integration.sh --build --tfs "Integration Tests" - displayName: Run integration tests (Electron) - timeoutInMinutes: 20 - - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ - ./scripts/test-web-integration.sh --browser webkit - displayName: Run integration tests (Browser, Webkit) - timeoutInMinutes: 20 - - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - script: | - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - APP_NAME="`ls $APP_ROOT | head -n 1`" - INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ - ./scripts/test-remote-integration.sh - displayName: Run integration tests (Remote) - timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + ./scripts/test-integration.sh --tfs "Integration Tests" + displayName: Run integration tests (Electron) + timeoutInMinutes: 20 + + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - 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 - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - script: | @@ -98,35 +122,44 @@ steps: continueOnError: true condition: succeededOrFailed() - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --web --tracing --headless - timeoutInMinutes: 20 - displayName: Run smoke tests (Browser, Chromium) + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + yarn --cwd test/smoke compile + displayName: Compile smoke tests + + - script: | + set -e + yarn smoketest-no-compile --tracing + timeoutInMinutes: 20 + displayName: Run smoke tests (Electron) + + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - 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 + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ + yarn smoketest-no-compile --web --tracing --headless + timeoutInMinutes: 20 + displayName: Run smoke tests (Browser, Chromium) + + - script: | + set -e + yarn gulp compile-extension:vscode-test-resolver + APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) + APP_NAME="`ls $APP_ROOT | head -n 1`" + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ + yarn smoketest-no-compile --tracing --remote --build "$APP_ROOT/$APP_NAME" + timeoutInMinutes: 20 + displayName: Run smoke tests (Remote) - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - script: | - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - APP_NAME="`ls $APP_ROOT | head -n 1`" - yarn smoketest-no-compile --tracing --build "$APP_ROOT/$APP_NAME" - timeoutInMinutes: 20 - displayName: Run smoke tests (Electron) - - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - script: | - set -e - yarn gulp compile-extension:vscode-test-resolver - APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - APP_NAME="`ls $APP_ROOT | head -n 1`" - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --tracing --remote --build "$APP_ROOT/$APP_NAME" - timeoutInMinutes: 20 - displayName: Run smoke tests (Remote) - - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - script: | set -e ps -ef @@ -148,7 +181,6 @@ steps: continueOnError: true condition: failed() - - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - task: PublishPipelineArtifact@0 @@ -164,7 +196,6 @@ steps: continueOnError: true condition: failed() - - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - task: PublishPipelineArtifact@0 inputs: targetPath: .build/logs diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index c9503f42a0a..eda79c53cf9 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -11,6 +11,11 @@ parameters: type: boolean steps: + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - checkout: self + fetchDepth: 1 + retryCountOnTaskFailure: 3 + - task: NodeTool@0 inputs: versionSpec: "16.x" @@ -23,16 +28,18 @@ steps: 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 + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - 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 + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz + displayName: Extract compilation output - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | @@ -123,11 +130,12 @@ steps: 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 + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn gulp vscode-darwin-$(VSCODE_ARCH)-min-ci + displayName: Build client - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | @@ -135,17 +143,26 @@ steps: 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 + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - 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 + + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn gulp "transpile-client" "transpile-extensions" + displayName: Transpile - ${{ if or(eq(parameters.VSCODE_RUN_UNIT_TESTS, true), eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - template: product-build-darwin-test.yml parameters: + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_RUN_UNIT_TESTS: ${{ parameters.VSCODE_RUN_UNIT_TESTS }} VSCODE_RUN_INTEGRATION_TESTS: ${{ parameters.VSCODE_RUN_INTEGRATION_TESTS }} VSCODE_RUN_SMOKE_TESTS: ${{ parameters.VSCODE_RUN_SMOKE_TESTS }} diff --git a/build/azure-pipelines/linux/product-build-linux-client-test.yml b/build/azure-pipelines/linux/product-build-linux-client-test.yml index bc9aae42daf..31d477e93aa 100644 --- a/build/azure-pipelines/linux/product-build-linux-client-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-client-test.yml @@ -1,4 +1,6 @@ parameters: + - name: VSCODE_QUALITY + type: string - name: VSCODE_RUN_UNIT_TESTS type: boolean - name: VSCODE_RUN_INTEGRATION_TESTS @@ -13,38 +15,68 @@ steps: yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" displayName: Download Electron and Playwright - - script: | - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - ELECTRON_ROOT=.build/electron - sudo chown root $APP_ROOT/chrome-sandbox - sudo chown root $ELECTRON_ROOT/chrome-sandbox - sudo chmod 4755 $APP_ROOT/chrome-sandbox - sudo chmod 4755 $ELECTRON_ROOT/chrome-sandbox - stat $APP_ROOT/chrome-sandbox - stat $ELECTRON_ROOT/chrome-sandbox - displayName: Change setuid helper binary permission - - - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - script: | set -e - ./scripts/test.sh --build --tfs "Unit Tests" - displayName: Run unit tests (Electron) - timeoutInMinutes: 15 + sudo apt-get update + sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libgbm1 + sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb + sudo chmod +x /etc/init.d/xvfb + sudo update-rc.d xvfb defaults + sudo service xvfb start + displayName: Setup build environment - - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | set -e - yarn test-node --build - displayName: Run unit tests (node.js) - timeoutInMinutes: 15 + APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) + ELECTRON_ROOT=.build/electron + sudo chown root $APP_ROOT/chrome-sandbox + sudo chown root $ELECTRON_ROOT/chrome-sandbox + sudo chmod 4755 $APP_ROOT/chrome-sandbox + sudo chmod 4755 $ELECTRON_ROOT/chrome-sandbox + stat $APP_ROOT/chrome-sandbox + stat $ELECTRON_ROOT/chrome-sandbox + displayName: Change setuid helper binary permission - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - - script: | - set -e - DEBUG=*browser* yarn test-browser-no-install --build --browser chromium --tfs "Browser Unit Tests" - displayName: Run unit tests (Browser, Chromium) - timeoutInMinutes: 15 + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" + displayName: Run unit tests (Electron) + timeoutInMinutes: 15 + + - script: | + set -e + yarn test-node + displayName: Run unit tests (node.js) + timeoutInMinutes: 15 + + - script: | + set -e + DEBUG=*browser* yarn test-browser-no-install --browser chromium --tfs "Browser Unit Tests" + displayName: Run unit tests (Browser, Chromium) + timeoutInMinutes: 15 + + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - 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 --build --browser chromium --tfs "Browser Unit Tests" + displayName: Run unit tests (Browser, Chromium) + timeoutInMinutes: 15 - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - script: | @@ -70,39 +102,57 @@ steps: displayName: Build integration tests - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - script: | - # Figure out the full absolute path of the product we just built - # including the remote server and configure the integration tests - # to run with these builds instead of running out of sources. - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") - INTEGRATION_TEST_APP_NAME="$APP_NAME" \ - INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ - ./scripts/test-integration.sh --build --tfs "Integration Tests" - displayName: Run integration tests (Electron) - timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + DISPLAY=:10 ./scripts/test-integration.sh --tfs "Integration Tests" + displayName: Run integration tests (Electron) + timeoutInMinutes: 20 - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-$(VSCODE_ARCH)" \ - ./scripts/test-web-integration.sh --browser chromium - displayName: Run integration tests (Browser, Chromium) - timeoutInMinutes: 20 + - script: | + set -e + ./scripts/test-web-integration.sh --browser chromium + displayName: Run integration tests (Browser, Chromium) + timeoutInMinutes: 20 - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - script: | - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") - INTEGRATION_TEST_APP_NAME="$APP_NAME" \ - INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ - ./scripts/test-remote-integration.sh - displayName: Run integration tests (Remote) - timeoutInMinutes: 20 + - script: | + set -e + ./scripts/test-remote-integration.sh + displayName: Run integration tests (Remote) + timeoutInMinutes: 20 + + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + # Figure out the full absolute path of the product we just built + # including the remote server and configure the integration tests + # to run with these builds instead of running out of sources. + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) + APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") + INTEGRATION_TEST_APP_NAME="$APP_NAME" \ + INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ + ./scripts/test-integration.sh --build --tfs "Integration Tests" + displayName: Run integration tests (Electron) + timeoutInMinutes: 20 + + - script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-$(VSCODE_ARCH)" \ + ./scripts/test-web-integration.sh --browser chromium + displayName: Run integration tests (Browser, Chromium) + timeoutInMinutes: 20 + + - script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) + APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") + INTEGRATION_TEST_APP_NAME="$APP_NAME" \ + INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ + ./scripts/test-remote-integration.sh + displayName: Run integration tests (Remote) + timeoutInMinutes: 20 - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - script: | @@ -114,33 +164,55 @@ steps: continueOnError: true condition: succeededOrFailed() - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" - timeoutInMinutes: 20 - displayName: Run smoke tests (Browser, Chromium) + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + yarn --cwd test/smoke compile + displayName: Compile smoke tests - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - script: | - set -e - APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - yarn smoketest-no-compile --tracing --build "$APP_PATH" - timeoutInMinutes: 20 - displayName: Run smoke tests (Electron) + - script: | + set -e + yarn smoketest-no-compile --tracing + timeoutInMinutes: 20 + displayName: Run smoke tests (Electron) - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - script: | - set -e - yarn gulp compile-extension:vscode-test-resolver - APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --tracing --remote --build "$APP_PATH" - timeoutInMinutes: 20 - displayName: Run smoke tests (Remote) + - script: | + set -e + yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" + timeoutInMinutes: 20 + displayName: Run smoke tests (Browser, Chromium) + + - script: | + set -e + yarn gulp compile-extension:vscode-test-resolver + yarn smoketest-no-compile --remote --tracing + timeoutInMinutes: 20 + displayName: Run smoke tests (Remote) + + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) + yarn smoketest-no-compile --tracing --build "$APP_PATH" + timeoutInMinutes: 20 + displayName: Run smoke tests (Electron) + + - script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-$(VSCODE_ARCH)" \ + yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" + timeoutInMinutes: 20 + displayName: Run smoke tests (Browser, Chromium) + + - script: | + set -e + yarn gulp compile-extension:vscode-test-resolver + APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ + yarn smoketest-no-compile --tracing --remote --build "$APP_PATH" + timeoutInMinutes: 20 + displayName: Run smoke tests (Remote) - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - script: | set -e ps -ef @@ -164,7 +236,6 @@ steps: continueOnError: true condition: failed() - - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - task: PublishPipelineArtifact@0 @@ -180,7 +251,6 @@ steps: continueOnError: true condition: failed() - - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - task: PublishPipelineArtifact@0 inputs: targetPath: .build/logs diff --git a/build/azure-pipelines/linux/product-build-linux-client.yml b/build/azure-pipelines/linux/product-build-linux-client.yml index ed4d853230b..1f57307739a 100644 --- a/build/azure-pipelines/linux/product-build-linux-client.yml +++ b/build/azure-pipelines/linux/product-build-linux-client.yml @@ -11,6 +11,11 @@ parameters: type: boolean steps: + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - checkout: self + fetchDepth: 1 + retryCountOnTaskFailure: 3 + - task: NodeTool@0 inputs: versionSpec: "16.x" @@ -23,33 +28,37 @@ steps: KeyVaultName: vscode SecretsFilter: "github-distro-mixin-password,ESRP-PKI,esrp-aad-username,esrp-aad-password" - - task: DownloadPipelineArtifact@2 - inputs: - artifact: Compilation - path: $(Build.ArtifactStagingDirectory) - displayName: Download compilation output + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - task: DownloadPipelineArtifact@2 + inputs: + artifact: Compilation + path: $(Build.ArtifactStagingDirectory) + displayName: Download compilation output - - task: DownloadPipelineArtifact@2 - inputs: - artifact: reh_node_modules-$(VSCODE_ARCH) - path: $(Build.ArtifactStagingDirectory) - displayName: Download server build dependencies - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'armhf')) + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - task: DownloadPipelineArtifact@2 + inputs: + artifact: reh_node_modules-$(VSCODE_ARCH) + path: $(Build.ArtifactStagingDirectory) + displayName: Download server build dependencies + condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'armhf')) - - script: | - set -e - # Start X server - /etc/init.d/xvfb start - # Start dbus session - DBUS_LAUNCH_RESULT=$(sudo dbus-daemon --config-file=/usr/share/dbus-1/system.conf --print-address) - echo "##vso[task.setvariable variable=DBUS_SESSION_BUS_ADDRESS]$DBUS_LAUNCH_RESULT" - displayName: Setup system services - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + # Start X server + /etc/init.d/xvfb start + # Start dbus session + DBUS_LAUNCH_RESULT=$(sudo dbus-daemon --config-file=/usr/share/dbus-1/system.conf --print-address) + echo "##vso[task.setvariable variable=DBUS_SESSION_BUS_ADDRESS]$DBUS_LAUNCH_RESULT" + displayName: Setup system services + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) - - script: | - set -e - tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz - displayName: Extract compilation output + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz + displayName: Extract compilation output - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | @@ -169,12 +178,13 @@ steps: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: | - set -e - 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(), ne(variables['VSCODE_ARCH'], 'armhf')) + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + 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(), ne(variables['VSCODE_ARCH'], 'armhf')) - script: | set -e @@ -190,11 +200,12 @@ steps: node build/azure-pipelines/mixin displayName: Mix in quality - - script: | - set -e - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - yarn gulp vscode-linux-$(VSCODE_ARCH)-min-ci - displayName: Build + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn gulp vscode-linux-$(VSCODE_ARCH)-min-ci + displayName: Build - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | @@ -202,17 +213,26 @@ steps: 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-linux-$(VSCODE_ARCH)-min-ci - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - yarn gulp vscode-reh-web-linux-$(VSCODE_ARCH)-min-ci - displayName: Build Server + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn gulp vscode-reh-linux-$(VSCODE_ARCH)-min-ci + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn gulp vscode-reh-web-linux-$(VSCODE_ARCH)-min-ci + displayName: Build Server + + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn gulp "transpile-client" "transpile-extensions" + displayName: Transpile - ${{ if or(eq(parameters.VSCODE_RUN_UNIT_TESTS, true), eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - template: product-build-linux-client-test.yml parameters: + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_RUN_UNIT_TESTS: ${{ parameters.VSCODE_RUN_UNIT_TESTS }} VSCODE_RUN_INTEGRATION_TESTS: ${{ parameters.VSCODE_RUN_INTEGRATION_TESTS }} VSCODE_RUN_SMOKE_TESTS: ${{ parameters.VSCODE_RUN_SMOKE_TESTS }} diff --git a/build/azure-pipelines/product-build-pr-cache.yml b/build/azure-pipelines/product-build-pr-cache.yml new file mode 100644 index 00000000000..067afa7492d --- /dev/null +++ b/build/azure-pipelines/product-build-pr-cache.yml @@ -0,0 +1,59 @@ +steps: + - checkout: self + fetchDepth: 1 + retryCountOnTaskFailure: 3 + + - task: NodeTool@0 + inputs: + versionSpec: "16.x" + + - 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: "genericNodeModules | $(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 + 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 + 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 diff --git a/build/azure-pipelines/product-build-pr.yml b/build/azure-pipelines/product-build-pr.yml index c8e23162f07..8f06bcfd09c 100644 --- a/build/azure-pipelines/product-build-pr.yml +++ b/build/azure-pipelines/product-build-pr.yml @@ -6,15 +6,6 @@ pr: branches: include: ["main", "release/*"] -resources: - containers: - - container: centos7-devtoolset8-x64 - image: vscodehub.azurecr.io/vscode-linux-build-agent:centos7-devtoolset8-x64 - options: --user 0:0 --cap-add SYS_ADMIN - - container: vscode-bionic-x64 - image: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-x64 - options: --user 0:0 --cap-add SYS_ADMIN - variables: - name: Codeql.SkipTaskAutoInjection value: true @@ -22,6 +13,8 @@ variables: value: true - name: ENABLE_TERRAPIN value: false + - name: VSCODE_CIBUILD + value: ${{ in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI') }} - name: VSCODE_PUBLISH value: false - name: VSCODE_QUALITY @@ -30,172 +23,173 @@ variables: value: false stages: - - stage: Compile - jobs: - - job: Compile - pool: vscode-1es-vscode-linux-18.04 - variables: - VSCODE_ARCH: x64 - steps: - - template: product-compile.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: + - stage: MaintainNodeModulesCache + displayName: Maintain node_modules cache + jobs: + - job: MaintainNodeModulesCache + displayName: Maintain node_modules cache + pool: vscode-1es-vscode-linux-20.04 + steps: + - template: product-build-pr-cache.yml - - stage: LinuxServerDependencies - dependsOn: [] - pool: vscode-1es-vscode-linux-18.04 - jobs: - - job: x64 - container: centos7-devtoolset8-x64 - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - steps: - - template: linux/product-build-linux-server.yml - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + - ${{ if ne(variables['VSCODE_CIBUILD'], true) }}: + - stage: Compile + displayName: Compile & Hygiene + jobs: + - job: Compile + displayName: Compile & Hygiene + pool: vscode-1es-vscode-linux-20.04 + variables: + VSCODE_ARCH: x64 + steps: + - template: product-compile.yml + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - - stage: Windows - dependsOn: - - Compile - pool: vscode-1es-vscode-windows-2019 - jobs: - - job: WindowsUnitTests - displayName: Unit Tests - timeoutInMinutes: 120 - variables: - VSCODE_ARCH: x64 - steps: - - template: win32/product-build-win32.yml - parameters: - VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: true - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - job: WindowsIntegrationTests - displayName: Integration Tests - timeoutInMinutes: 120 - variables: - VSCODE_ARCH: x64 - steps: - - template: win32/product-build-win32.yml - parameters: - VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: true - VSCODE_RUN_SMOKE_TESTS: false - - job: WindowsSmokeTests - displayName: Smoke Tests - timeoutInMinutes: 120 - variables: - VSCODE_ARCH: x64 - steps: - - template: win32/product-build-win32.yml - parameters: - VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: true + - stage: Test + dependsOn: [] + jobs: + - job: Linuxx64UnitTest + displayName: Linux (Unit Tests) + pool: vscode-1es-vscode-linux-20.04 + # container: vscode-bionic-x64 + timeoutInMinutes: 60 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: linux/product-build-linux-client.yml + parameters: + VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: true + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: false + - job: Linuxx64IntegrationTest + displayName: Linux (Integration Tests) + pool: vscode-1es-vscode-linux-20.04 + # container: vscode-bionic-x64 + timeoutInMinutes: 60 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: linux/product-build-linux-client.yml + parameters: + VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: true + VSCODE_RUN_SMOKE_TESTS: false + - job: Linuxx64SmokeTest + displayName: Linux (Smoke Tests) + pool: vscode-1es-vscode-linux-20.04 + # container: vscode-bionic-x64 + timeoutInMinutes: 60 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: linux/product-build-linux-client.yml + parameters: + VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: true - - stage: Linux - dependsOn: - - Compile - - LinuxServerDependencies - pool: vscode-1es-vscode-linux-18.04 - jobs: - - job: Linuxx64UnitTest - displayName: Unit Tests - container: vscode-bionic-x64 - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: linux/product-build-linux-client.yml - parameters: - VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: true - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - job: Linuxx64IntegrationTest - displayName: Integration Tests - container: vscode-bionic-x64 - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: linux/product-build-linux-client.yml - parameters: - VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: true - VSCODE_RUN_SMOKE_TESTS: false - - job: Linuxx64SmokeTest - displayName: Smoke Tests - container: vscode-bionic-x64 - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: linux/product-build-linux-client.yml - parameters: - VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: true + # - job: macOSUnitTest + # displayName: macOS (Unit Tests) + # pool: + # vmImage: macOS-latest + # timeoutInMinutes: 60 + # variables: + # BUILDSECMON_OPT_IN: true + # VSCODE_ARCH: x64 + # steps: + # - template: darwin/product-build-darwin.yml + # parameters: + # VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} + # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + # VSCODE_RUN_UNIT_TESTS: true + # VSCODE_RUN_INTEGRATION_TESTS: false + # VSCODE_RUN_SMOKE_TESTS: false + # - job: macOSIntegrationTest + # displayName: macOS (Integration Tests) + # pool: + # vmImage: macOS-latest + # timeoutInMinutes: 60 + # variables: + # BUILDSECMON_OPT_IN: true + # VSCODE_ARCH: x64 + # steps: + # - template: darwin/product-build-darwin.yml + # parameters: + # VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} + # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + # VSCODE_RUN_UNIT_TESTS: false + # VSCODE_RUN_INTEGRATION_TESTS: true + # VSCODE_RUN_SMOKE_TESTS: false + # - job: macOSSmokeTest + # displayName: macOS (Smoke Tests) + # pool: + # vmImage: macOS-latest + # timeoutInMinutes: 60 + # variables: + # BUILDSECMON_OPT_IN: true + # VSCODE_ARCH: x64 + # steps: + # - template: darwin/product-build-darwin.yml + # parameters: + # VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} + # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + # VSCODE_RUN_UNIT_TESTS: false + # VSCODE_RUN_INTEGRATION_TESTS: false + # VSCODE_RUN_SMOKE_TESTS: true - - stage: macOS - dependsOn: - - Compile - pool: - vmImage: macOS-latest - variables: - BUILDSECMON_OPT_IN: true - jobs: - - job: macOSUnitTest - displayName: Unit Tests - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: darwin/product-build-darwin.yml - parameters: - VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: true - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: false - - job: macOSIntegrationTest - displayName: Integration Tests - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: darwin/product-build-darwin.yml - parameters: - VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: true - VSCODE_RUN_SMOKE_TESTS: false - - job: macOSSmokeTest - displayName: Smoke Tests - timeoutInMinutes: 90 - variables: - VSCODE_ARCH: x64 - steps: - - template: darwin/product-build-darwin.yml - parameters: - VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: false - VSCODE_RUN_SMOKE_TESTS: true + # - job: WindowsUnitTests + # displayName: Windows (Unit Tests) + # pool: vscode-1es-vscode-windows-2019 + # timeoutInMinutes: 60 + # variables: + # VSCODE_ARCH: x64 + # steps: + # - template: win32/product-build-win32.yml + # parameters: + # VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} + # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + # VSCODE_RUN_UNIT_TESTS: true + # VSCODE_RUN_INTEGRATION_TESTS: false + # VSCODE_RUN_SMOKE_TESTS: false + # - job: WindowsIntegrationTests + # displayName: Windows (Integration Tests) + # pool: vscode-1es-vscode-windows-2019 + # timeoutInMinutes: 60 + # variables: + # VSCODE_ARCH: x64 + # steps: + # - template: win32/product-build-win32.yml + # parameters: + # VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} + # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + # VSCODE_RUN_UNIT_TESTS: false + # VSCODE_RUN_INTEGRATION_TESTS: true + # VSCODE_RUN_SMOKE_TESTS: false + # - job: WindowsSmokeTests + # displayName: Windows (Smoke Tests) + # pool: vscode-1es-vscode-windows-2019 + # timeoutInMinutes: 60 + # variables: + # VSCODE_ARCH: x64 + # steps: + # - template: win32/product-build-win32.yml + # parameters: + # VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }} + # VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + # VSCODE_RUN_UNIT_TESTS: false + # VSCODE_RUN_INTEGRATION_TESTS: false + # VSCODE_RUN_SMOKE_TESTS: true diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 1fd9b0441de..381d49ee75a 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -116,12 +116,13 @@ steps: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile & Hygiene - - script: | - set -e - yarn --cwd test/smoke compile - yarn --cwd test/integration/browser compile - displayName: Compile test suites - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + yarn --cwd test/smoke compile + yarn --cwd test/integration/browser compile + displayName: Compile test suites + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - task: AzureCLI@2 @@ -151,16 +152,18 @@ steps: ./build/azure-pipelines/common/extract-telemetry.sh displayName: Extract Telemetry - - script: | - set -e - tar -cz --ignore-failed-read -f $(Build.ArtifactStagingDirectory)/compilation.tar.gz .build out-* test/integration/browser/out test/smoke/out test/automation/out - displayName: Compress compilation artifact + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + tar -cz --ignore-failed-read -f $(Build.ArtifactStagingDirectory)/compilation.tar.gz .build out-* test/integration/browser/out test/smoke/out test/automation/out + displayName: Compress compilation artifact - - task: PublishPipelineArtifact@1 - inputs: - targetPath: $(Build.ArtifactStagingDirectory)/compilation.tar.gz - artifactName: Compilation - displayName: Publish compilation artifact + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - task: PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/compilation.tar.gz + artifactName: Compilation + displayName: Publish compilation artifact - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index 9dc50f8bcc4..59c91cd2b13 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -1,4 +1,6 @@ parameters: + - name: VSCODE_QUALITY + type: string - name: VSCODE_RUN_UNIT_TESTS type: boolean - name: VSCODE_RUN_INTEGRATION_TESTS @@ -15,29 +17,51 @@ steps: displayName: Download Electron and Playwright - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { yarn electron $(VSCODE_ARCH) } - exec { .\scripts\test.bat --build --tfs "Unit Tests" } - displayName: Run unit tests (Electron) - timeoutInMinutes: 15 - - - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { yarn test-node --build } - displayName: Run unit tests (node.js) - timeoutInMinutes: 15 - - - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { yarn test-browser-no-install --sequential --build --browser chromium --browser firefox --tfs "Browser Unit Tests" } - displayName: Run unit tests (Browser, Chromium & Firefox) - timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn electron $(VSCODE_ARCH) } + exec { .\scripts\test.bat --tfs "Unit Tests" } + displayName: Run unit tests (Electron) + timeoutInMinutes: 15 + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn test-node } + displayName: Run unit tests (node.js) + timeoutInMinutes: 15 + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { node test/unit/browser/index.js --sequential --browser chromium --browser firefox --tfs "Browser Unit Tests" } + displayName: Run unit tests (Browser, Chromium & Firefox) + timeoutInMinutes: 20 + + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn electron $(VSCODE_ARCH) } + exec { .\scripts\test.bat --build --tfs "Unit Tests" } + displayName: Run unit tests (Electron) + timeoutInMinutes: 15 + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn test-node --build } + displayName: Run unit tests (node.js) + timeoutInMinutes: 15 + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn test-browser-no-install --sequential --build --browser chromium --browser firefox --tfs "Browser Unit Tests" } + displayName: Run unit tests (Browser, Chromium & Firefox) + timeoutInMinutes: 20 - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - powershell: | @@ -64,38 +88,58 @@ steps: } displayName: Build integration tests - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - powershell: | - # Figure out the full absolute path of the product we just built - # including the remote server and configure the integration tests - # to run with these builds instead of running out of sources. - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json - $AppNameShort = $AppProductJson.nameShort - exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\scripts\test-integration.bat --build --tfs "Integration Tests" } - displayName: Run integration tests (Electron) - timeoutInMinutes: 20 - - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; .\scripts\test-web-integration.bat --browser firefox } - displayName: Run integration tests (Browser, Firefox) - timeoutInMinutes: 20 - - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json - $AppNameShort = $AppProductJson.nameShort - exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\scripts\test-remote-integration.bat } - displayName: Run integration tests (Remote) - timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { .\scripts\test-integration.bat --tfs "Integration Tests" } + displayName: Run integration tests (Electron) + timeoutInMinutes: 20 + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { .\scripts\test-web-integration.bat --browser firefox } + displayName: Run integration tests (Browser, Firefox) + timeoutInMinutes: 20 + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { .\scripts\test-remote-integration.bat } + displayName: Run integration tests (Remote) + timeoutInMinutes: 20 + + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - powershell: | + # Figure out the full absolute path of the product we just built + # including the remote server and configure the integration tests + # to run with these builds instead of running out of sources. + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json + $AppNameShort = $AppProductJson.nameShort + exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\scripts\test-integration.bat --build --tfs "Integration Tests" } + displayName: Run integration tests (Electron) + timeoutInMinutes: 20 + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; .\scripts\test-web-integration.bat --browser firefox } + displayName: Run integration tests (Browser, Firefox) + timeoutInMinutes: 20 + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json + $AppNameShort = $AppProductJson.nameShort + exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\scripts\test-remote-integration.bat } + displayName: Run integration tests (Remote) + timeoutInMinutes: 20 - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - powershell: | @@ -105,36 +149,47 @@ steps: continueOnError: true condition: succeededOrFailed() - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)" - exec { yarn smoketest-no-compile --web --tracing --headless } - displayName: Run smoke tests (Browser, Chromium) - timeoutInMinutes: 20 - - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - exec { yarn smoketest-no-compile --tracing --build "$AppRoot" } - displayName: Run smoke tests (Electron) - timeoutInMinutes: 20 - - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)" - exec { yarn gulp compile-extension:vscode-test-resolver } - exec { yarn smoketest-no-compile --tracing --remote --build "$AppRoot" } - displayName: Run smoke tests (Remote) - timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn --cwd test/smoke compile } + displayName: Compile smoke tests + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn smoketest-no-compile --tracing } + displayName: Run smoke tests (Electron) + timeoutInMinutes: 20 + + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + exec { yarn smoketest-no-compile --tracing --build "$AppRoot" } + displayName: Run smoke tests (Electron) + timeoutInMinutes: 20 + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)" + exec { yarn smoketest-no-compile --web --tracing --headless } + displayName: Run smoke tests (Browser, Chromium) + timeoutInMinutes: 20 + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)" + exec { yarn gulp compile-extension:vscode-test-resolver } + exec { yarn smoketest-no-compile --tracing --remote --build "$AppRoot" } + displayName: Run smoke tests (Remote) + timeoutInMinutes: 20 - - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: - powershell: | . build/azure-pipelines/win32/exec.ps1 exec {.\build\azure-pipelines\win32\listprocesses.bat } @@ -156,7 +211,6 @@ steps: continueOnError: true condition: failed() - - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - task: PublishPipelineArtifact@0 @@ -172,7 +226,6 @@ steps: continueOnError: true condition: failed() - - ${{ if or(eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - task: PublishPipelineArtifact@0 inputs: targetPath: .build\logs diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 65504f03ecd..41f0a8da8c2 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -11,6 +11,11 @@ parameters: type: boolean steps: + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - checkout: self + fetchDepth: 1 + retryCountOnTaskFailure: 3 + - task: NodeTool@0 inputs: versionSpec: "16.x" @@ -28,17 +33,19 @@ steps: KeyVaultName: vscode SecretsFilter: "github-distro-mixin-password,ESRP-PKI,esrp-aad-username,esrp-aad-password" - - task: DownloadPipelineArtifact@2 - inputs: - artifact: Compilation - path: $(Build.ArtifactStagingDirectory) - displayName: Download compilation output + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - task: DownloadPipelineArtifact@2 + inputs: + artifact: Compilation + path: $(Build.ArtifactStagingDirectory) + displayName: Download compilation output - - task: ExtractFiles@1 - displayName: Extract compilation output - inputs: - archiveFilePatterns: "$(Build.ArtifactStagingDirectory)/compilation.tar.gz" - cleanDestinationFolder: false + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - task: ExtractFiles@1 + displayName: Extract compilation output + inputs: + archiveFilePatterns: "$(Build.ArtifactStagingDirectory)/compilation.tar.gz" + cleanDestinationFolder: false - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - powershell: | @@ -69,6 +76,7 @@ steps: displayName: Merge distro - powershell: | + if (!(Test-Path ".build")) { New-Item -Path ".build" -ItemType Directory } "$(VSCODE_ARCH)" | Out-File -Encoding ascii -NoNewLine .build\arch "$env:ENABLE_TERRAPIN" | Out-File -Encoding ascii -NoNewLine .build\terrapin node build/azure-pipelines/common/computeNodeModulesCacheKey.js > .build/yarnlockhash @@ -127,20 +135,29 @@ steps: exec { node build/azure-pipelines/mixin } displayName: Mix in quality - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { node build\lib\policies } - displayName: Generate Group Policy definitions - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) + - ${{ if eq(parameters.VSCODE_PUBLISH, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { node build\lib\policies } + displayName: Generate Group Policy definitions - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" - exec { yarn gulp "vscode-win32-$(VSCODE_ARCH)-min-ci" } - echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)" - displayName: Build + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" + exec { yarn gulp "transpile-client" "transpile-extensions" } + displayName: Transpile + + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" + exec { yarn gulp "vscode-win32-$(VSCODE_ARCH)-min-ci" } + echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)" + displayName: Build - ${{ if eq(parameters.VSCODE_PUBLISH, true) }}: - powershell: | @@ -158,19 +175,21 @@ steps: displayName: Mix in quality condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" - exec { yarn gulp "vscode-reh-win32-$(VSCODE_ARCH)-min-ci" } - exec { yarn gulp "vscode-reh-web-win32-$(VSCODE_ARCH)-min-ci" } - echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(CodeSigningFolderPath),$(agent.builddirectory)/vscode-reh-win32-$(VSCODE_ARCH)" - displayName: Build Server - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" + exec { yarn gulp "vscode-reh-win32-$(VSCODE_ARCH)-min-ci" } + exec { yarn gulp "vscode-reh-web-win32-$(VSCODE_ARCH)-min-ci" } + echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(CodeSigningFolderPath),$(agent.builddirectory)/vscode-reh-win32-$(VSCODE_ARCH)" + displayName: Build Server + condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'arm64')) - ${{ if or(eq(parameters.VSCODE_RUN_UNIT_TESTS, true), eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true), eq(parameters.VSCODE_RUN_SMOKE_TESTS, true)) }}: - template: product-build-win32-test.yml parameters: + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} VSCODE_RUN_UNIT_TESTS: ${{ parameters.VSCODE_RUN_UNIT_TESTS }} VSCODE_RUN_INTEGRATION_TESTS: ${{ parameters.VSCODE_RUN_INTEGRATION_TESTS }} VSCODE_RUN_SMOKE_TESTS: ${{ parameters.VSCODE_RUN_SMOKE_TESTS }} diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 210556eddb6..489d9ccfabd 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -16,6 +16,9 @@ const task = require('./lib/task'); const packageJson = require('../package.json'); const product = require('../product.json'); const rpmDependenciesGenerator = require('./linux/rpm/dependencies-generator'); +const debianDependenciesGenerator = require('./linux/debian/dependencies-generator'); +const sysrootInstaller = require('./linux/debian/install-sysroot'); +const debianRecommendedDependencies = require('./linux/debian/dep-lists').recommendedDeps; const path = require('path'); const root = path.dirname(__dirname); const commit = util.getVersion(root); @@ -74,12 +77,16 @@ function prepareDebPackage(arch) { let size = 0; const control = code.pipe(es.through( function (f) { size += f.isDirectory() ? 4096 : f.contents.length; }, - function () { + async function () { const that = this; + const sysroot = await sysrootInstaller.getSysroot(debArch); + const dependencies = debianDependenciesGenerator.getDependencies(binaryDir, product.applicationName, debArch, sysroot); gulp.src('resources/linux/debian/control.template', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) .pipe(replace('@@VERSION@@', packageJson.version + '-' + linuxPackageRevision)) .pipe(replace('@@ARCHITECTURE@@', debArch)) + .pipe(replace('@@DEPENDS@@', dependencies.join(', '))) + .pipe(replace('@@RECOMMENDS@@', debianRecommendedDependencies.join(', '))) .pipe(replace('@@INSTALLEDSIZE@@', Math.ceil(size / 1024))) .pipe(rename('DEBIAN/control')) .pipe(es.through(function (f) { that.emit('data', f); }, function () { that.emit('end'); })); diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 21944c463f9..443073cb039 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -188,6 +188,15 @@ class MonacoGenerator { } } function generateApiProposalNames() { + let eol; + try { + const src = fs.readFileSync('src/vs/workbench/services/extensions/common/extensionsApiProposals.ts', 'utf-8'); + const match = /\r?\n/m.exec(src); + eol = match ? match[0] : os.EOL; + } + catch { + eol = os.EOL; + } const pattern = /vscode\.proposed\.([a-zA-Z]+)\.d\.ts$/; const proposalNames = new Set(); const input = es.through(); @@ -214,7 +223,7 @@ function generateApiProposalNames() { '});', 'export type ApiProposalName = keyof typeof allApiProposals;', '', - ].join(os.EOL); + ].join(eol); this.emit('data', new File({ path: 'vs/workbench/services/extensions/common/extensionsApiProposals.ts', contents: Buffer.from(contents) diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index b737a5e8746..de1947f2ae1 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -226,6 +226,16 @@ class MonacoGenerator { } function generateApiProposalNames() { + let eol: string; + + try { + const src = fs.readFileSync('src/vs/workbench/services/extensions/common/extensionsApiProposals.ts', 'utf-8'); + const match = /\r?\n/m.exec(src); + eol = match ? match[0] : os.EOL; + } catch { + eol = os.EOL; + } + const pattern = /vscode\.proposed\.([a-zA-Z]+)\.d\.ts$/; const proposalNames = new Set<string>(); @@ -254,7 +264,7 @@ function generateApiProposalNames() { '});', 'export type ApiProposalName = keyof typeof allApiProposals;', '', - ].join(os.EOL); + ].join(eol); this.emit('data', new File({ path: 'vs/workbench/services/extensions/common/extensionsApiProposals.ts', diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js index 8fc5930c024..9a11c6fd9cc 100644 --- a/build/lib/treeshaking.js +++ b/build/lib/treeshaking.js @@ -89,6 +89,8 @@ function discoverAndReadFiles(ts, options) { const in_queue = Object.create(null); const queue = []; const enqueue = (moduleId) => { + // To make the treeshaker work on windows... + moduleId = moduleId.replace(/\\/g, '/'); if (in_queue[moduleId]) { return; } diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index 1d2e1bd4381..eb4f6f7767a 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -142,6 +142,8 @@ function discoverAndReadFiles(ts: typeof import('typescript'), options: ITreeSha const queue: string[] = []; const enqueue = (moduleId: string) => { + // To make the treeshaker work on windows... + moduleId = moduleId.replace(/\\/g, '/'); if (in_queue[moduleId]) { return; } diff --git a/build/linux/debian/dep-lists.js b/build/linux/debian/dep-lists.js new file mode 100644 index 00000000000..3db2ef1abff --- /dev/null +++ b/build/linux/debian/dep-lists.js @@ -0,0 +1,156 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.referenceGeneratedDepsByArch = exports.bundledDeps = exports.recommendedDeps = exports.additionalDeps = void 0; +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/additional_deps +// Additional dependencies not in the dpkg-shlibdeps output. +exports.additionalDeps = [ + 'ca-certificates', + 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libnss3 (>= 3.26)', + 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', + 'xdg-utils (>= 1.0.2)' // OS integration +]; +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/manual_recommends +// Dependencies that we can only recommend +// for now since some of the older distros don't support them. +exports.recommendedDeps = [ + 'libvulkan1' // Move to additionalDeps once support for Trusty and Jessie are dropped. +]; +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/98.0.4758.109:chrome/installer/linux/BUILD.gn;l=64-80 +// and the Linux Archive build +// Shared library dependencies that we already bundle. +exports.bundledDeps = [ + 'libEGL.so', + 'libGLESv2.so', + 'libvulkan.so.1', + 'swiftshader_libEGL.so', + 'swiftshader_libGLESv2.so', + 'libvk_swiftshader.so', + 'libffmpeg.so' +]; +exports.referenceGeneratedDepsByArch = { + 'amd64': [ + 'ca-certificates', + 'libasound2 (>= 1.0.16)', + 'libatk-bridge2.0-0 (>= 2.5.3)', + 'libatk1.0-0 (>= 2.2.0)', + 'libatspi2.0-0 (>= 2.9.90)', + 'libc6 (>= 2.14)', + 'libc6 (>= 2.17)', + 'libc6 (>= 2.2.5)', + 'libcairo2 (>= 1.6.0)', + 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', + 'libdbus-1-3 (>= 1.5.12)', + 'libdrm2 (>= 2.4.38)', + 'libexpat1 (>= 2.0.1)', + 'libgbm1 (>= 8.1~0)', + 'libgcc1 (>= 1:3.0)', + 'libglib2.0-0 (>= 2.16.0)', + 'libglib2.0-0 (>= 2.39.4)', + 'libgtk-3-0 (>= 3.9.10)', + 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libnspr4 (>= 2:4.9-2~)', + 'libnss3 (>= 2:3.22)', + 'libnss3 (>= 3.26)', + 'libpango-1.0-0 (>= 1.14.0)', + 'libsecret-1-0 (>= 0.18)', + 'libx11-6', + 'libx11-6 (>= 2:1.4.99.1)', + 'libxcb1 (>= 1.9.2)', + 'libxcomposite1 (>= 1:0.4.4-1)', + 'libxdamage1 (>= 1:1.1)', + 'libxext6', + 'libxfixes3', + 'libxkbcommon0 (>= 0.4.1)', + 'libxkbfile1', + 'libxrandr2', + 'xdg-utils (>= 1.0.2)' + ], + 'armhf': [ + 'ca-certificates', + 'libasound2 (>= 1.0.16)', + 'libatk-bridge2.0-0 (>= 2.5.3)', + 'libatk1.0-0 (>= 2.2.0)', + 'libatspi2.0-0 (>= 2.9.90)', + 'libc6 (>= 2.17)', + 'libc6 (>= 2.4)', + 'libc6 (>= 2.9)', + 'libcairo2 (>= 1.6.0)', + 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', + 'libdbus-1-3 (>= 1.5.12)', + 'libdrm2 (>= 2.4.38)', + 'libexpat1 (>= 2.0.1)', + 'libgbm1 (>= 8.1~0)', + 'libgcc1 (>= 1:3.0)', + 'libgcc1 (>= 1:3.5)', + 'libglib2.0-0 (>= 2.16.0)', + 'libglib2.0-0 (>= 2.39.4)', + 'libgtk-3-0 (>= 3.9.10)', + 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libnspr4 (>= 2:4.9-2~)', + 'libnss3 (>= 2:3.22)', + 'libnss3 (>= 3.26)', + 'libpango-1.0-0 (>= 1.14.0)', + 'libsecret-1-0 (>= 0.18)', + 'libstdc++6 (>= 4.1.1)', + 'libstdc++6 (>= 5)', + 'libstdc++6 (>= 5.2)', + 'libstdc++6 (>= 6)', + 'libx11-6', + 'libx11-6 (>= 2:1.4.99.1)', + 'libxcb1 (>= 1.9.2)', + 'libxcomposite1 (>= 1:0.4.4-1)', + 'libxdamage1 (>= 1:1.1)', + 'libxext6', + 'libxfixes3', + 'libxkbcommon0 (>= 0.4.1)', + 'libxkbfile1', + 'libxrandr2', + 'xdg-utils (>= 1.0.2)' + ], + 'arm64': [ + 'ca-certificates', + 'libasound2 (>= 1.0.16)', + 'libatk-bridge2.0-0 (>= 2.5.3)', + 'libatk1.0-0 (>= 2.2.0)', + 'libatspi2.0-0 (>= 2.9.90)', + 'libc6 (>= 2.17)', + 'libcairo2 (>= 1.6.0)', + 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', + 'libdbus-1-3 (>= 1.0.2)', + 'libdrm2 (>= 2.4.38)', + 'libexpat1 (>= 2.0.1)', + 'libgbm1 (>= 8.1~0)', + 'libgcc1 (>= 1:3.0)', + 'libgcc1 (>= 1:4.2)', + 'libgcc1 (>= 1:4.5)', + 'libglib2.0-0 (>= 2.16.0)', + 'libglib2.0-0 (>= 2.39.4)', + 'libgtk-3-0 (>= 3.9.10)', + 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libnspr4 (>= 2:4.9-2~)', + 'libnss3 (>= 2:3.22)', + 'libnss3 (>= 3.26)', + 'libpango-1.0-0 (>= 1.14.0)', + 'libsecret-1-0 (>= 0.18)', + 'libstdc++6 (>= 4.1.1)', + 'libstdc++6 (>= 5)', + 'libstdc++6 (>= 5.2)', + 'libstdc++6 (>= 6)', + 'libx11-6', + 'libx11-6 (>= 2:1.4.99.1)', + 'libxcb1 (>= 1.9.2)', + 'libxcomposite1 (>= 1:0.4.4-1)', + 'libxdamage1 (>= 1:1.1)', + 'libxext6', + 'libxfixes3', + 'libxkbcommon0 (>= 0.4.1)', + 'libxkbfile1', + 'libxrandr2', + 'xdg-utils (>= 1.0.2)' + ] +}; diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts new file mode 100644 index 00000000000..1e1b0e5b63f --- /dev/null +++ b/build/linux/debian/dep-lists.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. + *--------------------------------------------------------------------------------------------*/ + +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/additional_deps +// Additional dependencies not in the dpkg-shlibdeps output. +export const additionalDeps = [ + 'ca-certificates', // Make sure users have SSL certificates. + 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libnss3 (>= 3.26)', + 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', // For Breakpad crash reports. + 'xdg-utils (>= 1.0.2)' // OS integration +]; + +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/manual_recommends +// Dependencies that we can only recommend +// for now since some of the older distros don't support them. +export const recommendedDeps = [ + 'libvulkan1' // Move to additionalDeps once support for Trusty and Jessie are dropped. +]; + +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/98.0.4758.109:chrome/installer/linux/BUILD.gn;l=64-80 +// and the Linux Archive build +// Shared library dependencies that we already bundle. +export const bundledDeps = [ + 'libEGL.so', + 'libGLESv2.so', + 'libvulkan.so.1', + 'swiftshader_libEGL.so', + 'swiftshader_libGLESv2.so', + 'libvk_swiftshader.so', + 'libffmpeg.so' +]; + +export const referenceGeneratedDepsByArch = { + 'amd64': [ + 'ca-certificates', + 'libasound2 (>= 1.0.16)', + 'libatk-bridge2.0-0 (>= 2.5.3)', + 'libatk1.0-0 (>= 2.2.0)', + 'libatspi2.0-0 (>= 2.9.90)', + 'libc6 (>= 2.14)', + 'libc6 (>= 2.17)', + 'libc6 (>= 2.2.5)', + 'libcairo2 (>= 1.6.0)', + 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', + 'libdbus-1-3 (>= 1.5.12)', + 'libdrm2 (>= 2.4.38)', + 'libexpat1 (>= 2.0.1)', + 'libgbm1 (>= 8.1~0)', + 'libgcc1 (>= 1:3.0)', + 'libglib2.0-0 (>= 2.16.0)', + 'libglib2.0-0 (>= 2.39.4)', + 'libgtk-3-0 (>= 3.9.10)', + 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libnspr4 (>= 2:4.9-2~)', + 'libnss3 (>= 2:3.22)', + 'libnss3 (>= 3.26)', + 'libpango-1.0-0 (>= 1.14.0)', + 'libsecret-1-0 (>= 0.18)', + 'libx11-6', + 'libx11-6 (>= 2:1.4.99.1)', + 'libxcb1 (>= 1.9.2)', + 'libxcomposite1 (>= 1:0.4.4-1)', + 'libxdamage1 (>= 1:1.1)', + 'libxext6', + 'libxfixes3', + 'libxkbcommon0 (>= 0.4.1)', + 'libxkbfile1', + 'libxrandr2', + 'xdg-utils (>= 1.0.2)' + ], + 'armhf': [ + 'ca-certificates', + 'libasound2 (>= 1.0.16)', + 'libatk-bridge2.0-0 (>= 2.5.3)', + 'libatk1.0-0 (>= 2.2.0)', + 'libatspi2.0-0 (>= 2.9.90)', + 'libc6 (>= 2.17)', + 'libc6 (>= 2.4)', + 'libc6 (>= 2.9)', + 'libcairo2 (>= 1.6.0)', + 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', + 'libdbus-1-3 (>= 1.5.12)', + 'libdrm2 (>= 2.4.38)', + 'libexpat1 (>= 2.0.1)', + 'libgbm1 (>= 8.1~0)', + 'libgcc1 (>= 1:3.0)', + 'libgcc1 (>= 1:3.5)', + 'libglib2.0-0 (>= 2.16.0)', + 'libglib2.0-0 (>= 2.39.4)', + 'libgtk-3-0 (>= 3.9.10)', + 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libnspr4 (>= 2:4.9-2~)', + 'libnss3 (>= 2:3.22)', + 'libnss3 (>= 3.26)', + 'libpango-1.0-0 (>= 1.14.0)', + 'libsecret-1-0 (>= 0.18)', + 'libstdc++6 (>= 4.1.1)', + 'libstdc++6 (>= 5)', + 'libstdc++6 (>= 5.2)', + 'libstdc++6 (>= 6)', + 'libx11-6', + 'libx11-6 (>= 2:1.4.99.1)', + 'libxcb1 (>= 1.9.2)', + 'libxcomposite1 (>= 1:0.4.4-1)', + 'libxdamage1 (>= 1:1.1)', + 'libxext6', + 'libxfixes3', + 'libxkbcommon0 (>= 0.4.1)', + 'libxkbfile1', + 'libxrandr2', + 'xdg-utils (>= 1.0.2)' + ], + 'arm64': [ + 'ca-certificates', + 'libasound2 (>= 1.0.16)', + 'libatk-bridge2.0-0 (>= 2.5.3)', + 'libatk1.0-0 (>= 2.2.0)', + 'libatspi2.0-0 (>= 2.9.90)', + 'libc6 (>= 2.17)', + 'libcairo2 (>= 1.6.0)', + 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', + 'libdbus-1-3 (>= 1.0.2)', + 'libdrm2 (>= 2.4.38)', + 'libexpat1 (>= 2.0.1)', + 'libgbm1 (>= 8.1~0)', + 'libgcc1 (>= 1:3.0)', + 'libgcc1 (>= 1:4.2)', + 'libgcc1 (>= 1:4.5)', + 'libglib2.0-0 (>= 2.16.0)', + 'libglib2.0-0 (>= 2.39.4)', + 'libgtk-3-0 (>= 3.9.10)', + 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', + 'libnspr4 (>= 2:4.9-2~)', + 'libnss3 (>= 2:3.22)', + 'libnss3 (>= 3.26)', + 'libpango-1.0-0 (>= 1.14.0)', + 'libsecret-1-0 (>= 0.18)', + 'libstdc++6 (>= 4.1.1)', + 'libstdc++6 (>= 5)', + 'libstdc++6 (>= 5.2)', + 'libstdc++6 (>= 6)', + 'libx11-6', + 'libx11-6 (>= 2:1.4.99.1)', + 'libxcb1 (>= 1.9.2)', + 'libxcomposite1 (>= 1:0.4.4-1)', + 'libxdamage1 (>= 1:1.1)', + 'libxext6', + 'libxfixes3', + 'libxkbcommon0 (>= 0.4.1)', + 'libxkbfile1', + 'libxrandr2', + 'xdg-utils (>= 1.0.2)' + ] +}; diff --git a/build/linux/debian/dependencies-generator.js b/build/linux/debian/dependencies-generator.js new file mode 100644 index 00000000000..235ca268531 --- /dev/null +++ b/build/linux/debian/dependencies-generator.js @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getDependencies = void 0; +const child_process_1 = require("child_process"); +const fs_1 = require("fs"); +const os_1 = require("os"); +const path = require("path"); +const dep_lists_1 = require("./dep-lists"); +// 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 Debian 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, sysroot) { + // Get the files for which we want to find dependencies. + const nativeModulesPath = path.join(buildDir, 'resources', 'app', 'node_modules.asar.unpacked'); + const findResult = (0, child_process_1.spawnSync)('find', [nativeModulesPath, '-name', '*.node']); + if (findResult.status) { + console.error('Error finding files:'); + console.error(findResult.stderr.toString()); + return []; + } + const files = findResult.stdout.toString().trimEnd().split('\n'); + const appPath = path.join(buildDir, applicationName); + files.push(appPath); + // Add chrome sandbox and crashpad handler. + files.push(path.join(buildDir, 'chrome-sandbox')); + files.push(path.join(buildDir, 'chrome_crashpad_handler')); + // Generate the dependencies. + const dependencies = files.map((file) => calculatePackageDeps(file, arch, sysroot)); + // Add additional dependencies. + const additionalDepsSet = new Set(dep_lists_1.additionalDeps); + dependencies.push(additionalDepsSet); + // Merge all the dependencies. + const mergedDependencies = mergePackageDeps(dependencies); + let sortedDependencies = []; + for (const dependency of mergedDependencies) { + sortedDependencies.push(dependency); + } + sortedDependencies.sort(); + // Exclude bundled dependencies + sortedDependencies = sortedDependencies.filter(dependency => { + return !dep_lists_1.bundledDeps.some(bundledDep => dependency.startsWith(bundledDep)); + }); + const referenceGeneratedDeps = dep_lists_1.referenceGeneratedDepsByArch[arch]; + if (JSON.stringify(sortedDependencies) !== JSON.stringify(referenceGeneratedDeps)) { + const failMessage = 'The dependencies list has changed.' + + '\nOld:\n' + referenceGeneratedDeps.join('\n') + + '\nNew:\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/debian/calculate_package_deps.py. +function calculatePackageDeps(binaryPath, arch, sysroot) { + try { + if (!((0, fs_1.statSync)(binaryPath).mode & fs_1.constants.S_IXUSR)) { + throw new Error(`Binary ${binaryPath} needs to have an executable bit set.`); + } + } + catch (e) { + // The package might not exist. Don't re-throw the error here. + console.error('Tried to stat ' + binaryPath + ' but failed.'); + } + // Get the Chromium dpkg-shlibdeps file. + const dpkgShlibdepsUrl = 'https://raw.githubusercontent.com/chromium/chromium/100.0.4896.160/third_party/dpkg-shlibdeps/dpkg-shlibdeps.pl'; + const dpkgShlibdepsScriptLocation = `${(0, os_1.tmpdir)()}/dpkg-shlibdeps.pl`; + const result = (0, child_process_1.spawnSync)('curl', [dpkgShlibdepsUrl, '-o', dpkgShlibdepsScriptLocation]); + if (result.status !== 0) { + throw new Error('Cannot retrieve dpkg-shlibdeps. Stderr:\n' + result.stderr); + } + const cmd = [dpkgShlibdepsScriptLocation, '--ignore-weak-undefined']; + switch (arch) { + case 'amd64': + cmd.push(`-l${sysroot}/usr/lib/x86_64-linux-gnu`, `-l${sysroot}/lib/x86_64-linux-gnu`); + break; + case 'armhf': + cmd.push(`-l${sysroot}/usr/lib/arm-linux-gnueabihf`, `-l${sysroot}/lib/arm-linux-gnueabihf`); + break; + case 'arm64': + cmd.push(`-l${sysroot}/usr/lib/aarch64-linux-gnu`, `-l${sysroot}/lib/aarch64-linux-gnu`); + break; + default: + throw new Error('Unsupported architecture ' + arch); + } + cmd.push(`-l${sysroot}/usr/lib`); + cmd.push('-O', '-e', path.resolve(binaryPath)); + const dpkgShlibdepsResult = (0, child_process_1.spawnSync)('perl', cmd, { cwd: sysroot }); + if (dpkgShlibdepsResult.status !== 0) { + throw new Error(`dpkg-shlibdeps failed with exit code ${dpkgShlibdepsResult.status}. stderr:\n${dpkgShlibdepsResult.stderr} `); + } + const shlibsDependsPrefix = 'shlibs:Depends='; + const requiresList = dpkgShlibdepsResult.stdout.toString('utf-8').trimEnd().split('\n'); + let depsStr = ''; + for (const line of requiresList) { + if (line.startsWith(shlibsDependsPrefix)) { + depsStr = line.substring(shlibsDependsPrefix.length); + } + } + const requires = new Set(depsStr.split(', ').sort()); + return requires; +} +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py. +function mergePackageDeps(inputDeps) { + // For now, see if directly appending the dependencies helps. + const requires = new Set(); + for (const depSet of inputDeps) { + for (const dep of depSet) { + const trimmedDependency = dep.trim(); + if (trimmedDependency.length && !trimmedDependency.startsWith('#')) { + requires.add(trimmedDependency); + } + } + } + return requires; +} diff --git a/build/linux/debian/dependencies-generator.ts b/build/linux/debian/dependencies-generator.ts new file mode 100644 index 00000000000..9e3d466cfe0 --- /dev/null +++ b/build/linux/debian/dependencies-generator.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { spawnSync } from 'child_process'; +import { constants, statSync } from 'fs'; +import { tmpdir } from 'os'; +import path = require('path'); +import { additionalDeps, bundledDeps, referenceGeneratedDepsByArch } from './dep-lists'; +import { ArchString } from './types'; + +// 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 Debian 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, sysroot: string): string[] { + // Get the files for which we want to find dependencies. + const nativeModulesPath = path.join(buildDir, 'resources', 'app', 'node_modules.asar.unpacked'); + const findResult = spawnSync('find', [nativeModulesPath, '-name', '*.node']); + if (findResult.status) { + console.error('Error finding files:'); + console.error(findResult.stderr.toString()); + return []; + } + + const files = findResult.stdout.toString().trimEnd().split('\n'); + + const appPath = path.join(buildDir, applicationName); + files.push(appPath); + + // Add chrome sandbox and crashpad handler. + files.push(path.join(buildDir, 'chrome-sandbox')); + files.push(path.join(buildDir, 'chrome_crashpad_handler')); + + // Generate the dependencies. + const dependencies: Set<string>[] = files.map((file) => calculatePackageDeps(file, arch, sysroot)); + // Add additional dependencies. + const additionalDepsSet = new Set(additionalDeps); + dependencies.push(additionalDepsSet); + + // Merge all the dependencies. + const mergedDependencies = mergePackageDeps(dependencies); + let sortedDependencies: string[] = []; + for (const dependency of mergedDependencies) { + sortedDependencies.push(dependency); + } + sortedDependencies.sort(); + + // Exclude bundled dependencies + sortedDependencies = sortedDependencies.filter(dependency => { + return !bundledDeps.some(bundledDep => dependency.startsWith(bundledDep)); + }); + + const referenceGeneratedDeps = referenceGeneratedDepsByArch[arch]; + if (JSON.stringify(sortedDependencies) !== JSON.stringify(referenceGeneratedDeps)) { + const failMessage = 'The dependencies list has changed.' + + '\nOld:\n' + referenceGeneratedDeps.join('\n') + + '\nNew:\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/debian/calculate_package_deps.py. +function calculatePackageDeps(binaryPath: string, arch: ArchString, sysroot: string): Set<string> { + try { + if (!(statSync(binaryPath).mode & constants.S_IXUSR)) { + throw new Error(`Binary ${binaryPath} needs to have an executable bit set.`); + } + } catch (e) { + // The package might not exist. Don't re-throw the error here. + console.error('Tried to stat ' + binaryPath + ' but failed.'); + } + + // Get the Chromium dpkg-shlibdeps file. + const dpkgShlibdepsUrl = 'https://raw.githubusercontent.com/chromium/chromium/100.0.4896.160/third_party/dpkg-shlibdeps/dpkg-shlibdeps.pl'; + const dpkgShlibdepsScriptLocation = `${tmpdir()}/dpkg-shlibdeps.pl`; + const result = spawnSync('curl', [dpkgShlibdepsUrl, '-o', dpkgShlibdepsScriptLocation]); + if (result.status !== 0) { + throw new Error('Cannot retrieve dpkg-shlibdeps. Stderr:\n' + result.stderr); + } + const cmd = [dpkgShlibdepsScriptLocation, '--ignore-weak-undefined']; + switch (arch) { + case 'amd64': + cmd.push(`-l${sysroot}/usr/lib/x86_64-linux-gnu`, + `-l${sysroot}/lib/x86_64-linux-gnu`); + break; + case 'armhf': + cmd.push(`-l${sysroot}/usr/lib/arm-linux-gnueabihf`, + `-l${sysroot}/lib/arm-linux-gnueabihf`); + break; + case 'arm64': + cmd.push(`-l${sysroot}/usr/lib/aarch64-linux-gnu`, + `-l${sysroot}/lib/aarch64-linux-gnu`); + break; + default: + throw new Error('Unsupported architecture ' + arch); + } + cmd.push(`-l${sysroot}/usr/lib`); + cmd.push('-O', '-e', path.resolve(binaryPath)); + + const dpkgShlibdepsResult = spawnSync('perl', cmd, { cwd: sysroot }); + if (dpkgShlibdepsResult.status !== 0) { + throw new Error(`dpkg-shlibdeps failed with exit code ${dpkgShlibdepsResult.status}. stderr:\n${dpkgShlibdepsResult.stderr} `); + } + + const shlibsDependsPrefix = 'shlibs:Depends='; + const requiresList = dpkgShlibdepsResult.stdout.toString('utf-8').trimEnd().split('\n'); + let depsStr = ''; + for (const line of requiresList) { + if (line.startsWith(shlibsDependsPrefix)) { + depsStr = line.substring(shlibsDependsPrefix.length); + } + } + const requires = new Set(depsStr.split(', ').sort()); + return requires; +} + +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py. +function mergePackageDeps(inputDeps: Set<string>[]): Set<string> { + // For now, see if directly appending the dependencies helps. + const requires = new Set<string>(); + for (const depSet of inputDeps) { + for (const dep of depSet) { + const trimmedDependency = dep.trim(); + if (trimmedDependency.length && !trimmedDependency.startsWith('#')) { + requires.add(trimmedDependency); + } + } + } + return requires; +} diff --git a/build/linux/debian/install-sysroot.js b/build/linux/debian/install-sysroot.js new file mode 100644 index 00000000000..c6b57a7b8a3 --- /dev/null +++ b/build/linux/debian/install-sysroot.js @@ -0,0 +1,81 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getSysroot = void 0; +const child_process_1 = require("child_process"); +const crypto_1 = require("crypto"); +const os_1 = require("os"); +const fs = require("fs"); +const https = require("https"); +const path = require("path"); +const sysroots_1 = require("./sysroots"); +// Based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/install-sysroot.py. +const URL_PREFIX = 'https://msftelectron.blob.core.windows.net'; +const URL_PATH = 'sysroots/toolchain'; +function getSha(filename) { + const hash = (0, crypto_1.createHash)('sha1'); + // Read file 1 MB at a time + const fd = fs.openSync(filename, 'r'); + const buffer = Buffer.alloc(1024 * 1024); + let position = 0; + let bytesRead = 0; + while ((bytesRead = fs.readSync(fd, buffer, 0, buffer.length, position)) === buffer.length) { + hash.update(buffer); + position += bytesRead; + } + hash.update(buffer.slice(0, bytesRead)); + return hash.digest('hex'); +} +async function getSysroot(arch) { + const sysrootDict = sysroots_1.sysrootInfo[arch]; + const tarballFilename = sysrootDict['Tarball']; + const tarballSha = sysrootDict['Sha1Sum']; + const sysroot = path.join((0, os_1.tmpdir)(), sysrootDict['SysrootDir']); + const url = [URL_PREFIX, URL_PATH, tarballSha, tarballFilename].join('/'); + const stamp = path.join(sysroot, '.stamp'); + if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === url) { + return sysroot; + } + console.log(`Installing Debian ${arch} root image: ${sysroot}`); + fs.rmSync(sysroot, { recursive: true, force: true }); + fs.mkdirSync(sysroot); + const tarball = path.join(sysroot, tarballFilename); + console.log(`Downloading ${url}`); + let downloadSuccess = false; + for (let i = 0; i < 3 && !downloadSuccess; i++) { + await new Promise((c) => { + https.get(url, (res) => { + const chunks = []; + res.on('data', (chunk) => { + chunks.push(chunk); + }); + res.on('end', () => { + fs.writeFileSync(tarball, Buffer.concat(chunks)); + downloadSuccess = true; + c(); + }); + }).on('error', (err) => { + console.error('Encountered an error during the download attempt: ' + err.message); + c(); + }); + }); + } + if (!downloadSuccess) { + throw new Error('Failed to download ' + url); + } + const sha = getSha(tarball); + if (sha !== tarballSha) { + throw new Error(`Tarball sha1sum is wrong. Expected ${tarballSha}, actual ${sha}`); + } + const proc = (0, child_process_1.spawnSync)('tar', ['xf', tarball, '-C', sysroot]); + if (proc.status) { + throw new Error('Tarball extraction failed with code ' + proc.status); + } + fs.rmSync(tarball); + fs.writeFileSync(stamp, url); + return sysroot; +} +exports.getSysroot = getSysroot; diff --git a/build/linux/debian/install-sysroot.ts b/build/linux/debian/install-sysroot.ts new file mode 100644 index 00000000000..7c04c44f730 --- /dev/null +++ b/build/linux/debian/install-sysroot.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { spawnSync } from 'child_process'; +import { createHash } from 'crypto'; +import { tmpdir } from 'os'; +import * as fs from 'fs'; +import * as https from 'https'; +import * as path from 'path'; +import { sysrootInfo } from './sysroots'; +import { ArchString } from './types'; + +// Based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/install-sysroot.py. +const URL_PREFIX = 'https://msftelectron.blob.core.windows.net'; +const URL_PATH = 'sysroots/toolchain'; + +function getSha(filename: fs.PathLike): string { + const hash = createHash('sha1'); + // Read file 1 MB at a time + const fd = fs.openSync(filename, 'r'); + const buffer = Buffer.alloc(1024 * 1024); + let position = 0; + let bytesRead = 0; + while ((bytesRead = fs.readSync(fd, buffer, 0, buffer.length, position)) === buffer.length) { + hash.update(buffer); + position += bytesRead; + } + hash.update(buffer.slice(0, bytesRead)); + return hash.digest('hex'); +} + +type SysrootDictEntry = { + Sha1Sum: string; + SysrootDir: string; + Tarball: string; +}; + +export async function getSysroot(arch: ArchString): Promise<string> { + const sysrootDict: SysrootDictEntry = sysrootInfo[arch]; + const tarballFilename = sysrootDict['Tarball']; + const tarballSha = sysrootDict['Sha1Sum']; + const sysroot = path.join(tmpdir(), sysrootDict['SysrootDir']); + const url = [URL_PREFIX, URL_PATH, tarballSha, tarballFilename].join('/'); + const stamp = path.join(sysroot, '.stamp'); + if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === url) { + return sysroot; + } + + console.log(`Installing Debian ${arch} root image: ${sysroot}`); + fs.rmSync(sysroot, { recursive: true, force: true }); + fs.mkdirSync(sysroot); + const tarball = path.join(sysroot, tarballFilename); + console.log(`Downloading ${url}`); + let downloadSuccess = false; + for (let i = 0; i < 3 && !downloadSuccess; i++) { + await new Promise<void>((c) => { + https.get(url, (res) => { + const chunks: Uint8Array[] = []; + res.on('data', (chunk) => { + chunks.push(chunk); + }); + res.on('end', () => { + fs.writeFileSync(tarball, Buffer.concat(chunks)); + downloadSuccess = true; + c(); + }); + }).on('error', (err) => { + console.error('Encountered an error during the download attempt: ' + err.message); + c(); + }); + }); + } + if (!downloadSuccess) { + throw new Error('Failed to download ' + url); + } + const sha = getSha(tarball); + if (sha !== tarballSha) { + throw new Error(`Tarball sha1sum is wrong. Expected ${tarballSha}, actual ${sha}`); + } + + const proc = spawnSync('tar', ['xf', tarball, '-C', sysroot]); + if (proc.status) { + throw new Error('Tarball extraction failed with code ' + proc.status); + } + fs.rmSync(tarball); + fs.writeFileSync(stamp, url); + return sysroot; +} diff --git a/build/linux/debian/sysroots.js b/build/linux/debian/sysroots.js new file mode 100644 index 00000000000..3825de44028 --- /dev/null +++ b/build/linux/debian/sysroots.js @@ -0,0 +1,26 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.sysrootInfo = void 0; +// Based on https://github.com/electron/electron/blob/main/script/sysroots.json, +// which itself is based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/sysroots.json. +exports.sysrootInfo = { + 'amd64': { + 'Sha1Sum': '7e008cea9eae822d80d55c67fbb5ef4204678e74', + 'SysrootDir': 'debian_sid_amd64-sysroot', + 'Tarball': 'debian_sid_amd64_sysroot.tar.xz' + }, + 'armhf': { + 'Sha1Sum': 'b6f4bb07817bea91b06514a9c1e3832df5a90dbf', + 'SysrootDir': 'debian_sid_arm-sysroot', + 'Tarball': 'debian_sid_arm_sysroot.tar.xz' + }, + 'arm64': { + 'Sha1Sum': '5a56c1ef714154ea5003bcafb16f21b0f8dde023', + 'SysrootDir': 'debian_sid_arm64-sysroot', + 'Tarball': 'debian_sid_arm64_sysroot.tar.xz' + } +}; diff --git a/build/linux/debian/sysroots.ts b/build/linux/debian/sysroots.ts new file mode 100644 index 00000000000..5fda6e6c7bb --- /dev/null +++ b/build/linux/debian/sysroots.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Based on https://github.com/electron/electron/blob/main/script/sysroots.json, +// which itself is based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/sysroots.json. +export const sysrootInfo = { + 'amd64': { + 'Sha1Sum': '7e008cea9eae822d80d55c67fbb5ef4204678e74', + 'SysrootDir': 'debian_sid_amd64-sysroot', + 'Tarball': 'debian_sid_amd64_sysroot.tar.xz' + }, + 'armhf': { + 'Sha1Sum': 'b6f4bb07817bea91b06514a9c1e3832df5a90dbf', + 'SysrootDir': 'debian_sid_arm-sysroot', + 'Tarball': 'debian_sid_arm_sysroot.tar.xz' + }, + 'arm64': { + 'Sha1Sum': '5a56c1ef714154ea5003bcafb16f21b0f8dde023', + 'SysrootDir': 'debian_sid_arm64-sysroot', + 'Tarball': 'debian_sid_arm64_sysroot.tar.xz' + } +}; diff --git a/build/linux/debian/types.js b/build/linux/debian/types.js new file mode 100644 index 00000000000..56d4e6c56ce --- /dev/null +++ b/build/linux/debian/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/debian/types.ts b/build/linux/debian/types.ts new file mode 100644 index 00000000000..ae2347cb12b --- /dev/null +++ b/build/linux/debian/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 = 'amd64' | 'armhf' | 'arm64'; diff --git a/build/linux/rpm/dependencies-generator.js b/build/linux/rpm/dependencies-generator.js index 3b1e404129c..90cfca96791 100644 --- a/build/linux/rpm/dependencies-generator.js +++ b/build/linux/rpm/dependencies-generator.js @@ -81,7 +81,7 @@ function calculatePackageDeps(binaryPath) { const requires = new Set(findRequiresResult.stdout.toString('utf-8').trimEnd().split('\n')); return requires; } -// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py. function mergePackageDeps(inputDeps) { const requires = new Set(); for (const depSet of inputDeps) { diff --git a/build/linux/rpm/dependencies-generator.ts b/build/linux/rpm/dependencies-generator.ts index e33210284dc..4b84640e280 100644 --- a/build/linux/rpm/dependencies-generator.ts +++ b/build/linux/rpm/dependencies-generator.ts @@ -92,7 +92,7 @@ function calculatePackageDeps(binaryPath: string): Set<string> { return requires; } -// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py. function mergePackageDeps(inputDeps: Set<string>[]): Set<string> { const requires = new Set<string>(); for (const depSet of inputDeps) { diff --git a/build/win32/code.iss b/build/win32/code.iss index e96ca4bde77..192bbb0d2ba 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -1494,7 +1494,7 @@ 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"'; + Permissions := Permissions + Format(' /grant:r "*S-1-3-0:(OI)(CI)F" /grant:r "%s:(OI)(CI)F"', [GetUserNameString()]); #endif Exec(ExpandConstant('{sys}\icacls.exe'), ExpandConstant('"{app}" /inheritancelevel:r ') + Permissions, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); diff --git a/extensions/configuration-editing/src/configurationEditingMain.ts b/extensions/configuration-editing/src/configurationEditingMain.ts index f63aa685cbe..224a57370b4 100644 --- a/extensions/configuration-editing/src/configurationEditingMain.ts +++ b/extensions/configuration-editing/src/configurationEditingMain.ts @@ -40,8 +40,8 @@ function registerVariableCompletions(pattern: string): vscode.Disposable { provideCompletionItems(document, position, _token) { const location = getLocation(document.getText(), document.offsetAt(position)); if (isCompletingInsidePropertyStringValue(document, location, position)) { - let range = document.getWordRangeAtPosition(position, /\$\{[^\}]*\}/); - if (!range || range.end.isEqual(position) || range.start.isEqual(position)) { + let range = document.getWordRangeAtPosition(position, /\$\{[^"\}]*\}?/); + if (!range || range.start.isEqual(position) || range.end.isEqual(position) && document.getText(range).endsWith('}')) { range = new vscode.Range(position, position); } diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index a4c0e3945d8..ab41c8220c2 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -96,8 +96,8 @@ export class SettingsDocument { return completions; } - let range = this.document.getWordRangeAtPosition(pos, /\$\{[^\}]*\}/); - if (!range || range.end.isEqual(pos) || range.start.isEqual(pos)) { + let range = this.document.getWordRangeAtPosition(pos, /\$\{[^"\}]*\}?/); + if (!range || range.start.isEqual(pos) || range.end.isEqual(pos) && this.document.getText(range).endsWith('}')) { range = new vscode.Range(pos, pos); } diff --git a/extensions/configuration-editing/src/test/completion.test.ts b/extensions/configuration-editing/src/test/completion.test.ts index 3df7024fa12..e5aec5fc46a 100644 --- a/extensions/configuration-editing/src/test/completion.test.ts +++ b/extensions/configuration-editing/src/test/completion.test.ts @@ -73,6 +73,20 @@ suite('Completions in settings.json', () => { const expected = { label: '${activeEditorMedium}', resultText }; await testCompletion(testFile, 'jsonc', content, expected); } + { // replacing a partial variable + const content = [ + '{', + ' "window.title": "${a|"', + '}', + ].join('\n'); + const resultText = [ + '{', + ' "window.title": "${dirty}"', + '}', + ].join('\n'); + const expected = { label: '${dirty}', resultText }; + await testCompletion(testFile, 'jsonc', content, expected); + } { // inserting a literal const content = [ '{', @@ -382,6 +396,32 @@ suite('Completions in launch.json', () => { const expected = { label: '${cwd}', resultText }; await testCompletion(testFile, 'jsonc', content, expected); } + { + const content = [ + '{', + ' "version": "0.2.0",', + ' "configurations": [', + ' {', + ' "name": "Do It",', + ' "program": "${workspace|"', + ' }', + ' ]', + '}', + ].join('\n'); + const resultText = [ + '{', + ' "version": "0.2.0",', + ' "configurations": [', + ' {', + ' "name": "Do It",', + ' "program": "${cwd}"', + ' }', + ' ]', + '}', + ].join('\n'); + const expected = { label: '${cwd}', resultText }; + await testCompletion(testFile, 'jsonc', content, expected); + } }); }); diff --git a/extensions/git/package.json b/extensions/git/package.json index 8a0a8cf7ed1..0b2195c6034 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -601,7 +601,7 @@ "command": "git.acceptMerge", "title": "%command.git.acceptMerge%", "category": "Git", - "enablement": "isMergeEditor" + "enablement": "isMergeEditor && mergeEditorResultUri in git.mergeChanges" } ], "keybindings": [ @@ -1549,7 +1549,7 @@ "merge/toolbar": [ { "command": "git.acceptMerge", - "when": "isMergeEditor && baseResourceScheme =~ /^git$|^file$/" + "when": "isMergeEditor && mergeEditorBaseUri =~ /^(git|file):/ && mergeEditorResultUri in git.mergeChanges" } ], "scm/change/title": [ @@ -2522,7 +2522,7 @@ }, "git.mergeEditor": { "type": "boolean", - "default": false, + "default": true, "markdownDescription": "%config.mergeEditor%", "scope": "window" } diff --git a/extensions/git/src/actionButton.ts b/extensions/git/src/actionButton.ts index 63ee474707b..527b5b1476c 100644 --- a/extensions/git/src/actionButton.ts +++ b/extensions/git/src/actionButton.ts @@ -17,6 +17,7 @@ interface ActionButtonState { readonly HEAD: Branch | undefined; readonly isCommitInProgress: boolean; readonly isMergeInProgress: boolean; + readonly isRebaseInProgress: boolean; readonly isSyncInProgress: boolean; readonly repositoryHasChangesToCommit: boolean; } @@ -43,6 +44,7 @@ export class ActionButtonCommand { HEAD: undefined, isCommitInProgress: false, isMergeInProgress: false, + isRebaseInProgress: false, isSyncInProgress: false, repositoryHasChangesToCommit: false }; @@ -70,7 +72,7 @@ export class ActionButtonCommand { } get button(): SourceControlActionButton | undefined { - if (!this.state.HEAD || !this.state.HEAD.name) { return undefined; } + if (!this.state.HEAD) { return undefined; } let actionButton: SourceControlActionButton | undefined; @@ -90,7 +92,25 @@ export class ActionButtonCommand { // The button is disabled if (!showActionButton.commit) { return undefined; } - let title: string, tooltip: string, commandArg: string; + return { + command: this.getCommitActionButtonPrimaryCommand(), + secondaryCommands: this.getCommitActionButtonSecondaryCommands(), + enabled: this.state.repositoryHasChangesToCommit && !this.state.isCommitInProgress && !this.state.isMergeInProgress + }; + } + + private getCommitActionButtonPrimaryCommand(): Command { + let commandArg = ''; + let title = localize('scm button commit title', "{0} Commit", '$(check)'); + let tooltip = this.state.isCommitInProgress ? localize('scm button committing tooltip', "Committing Changes...") : localize('scm button commit tooltip', "Commit Changes"); + + // Rebase Continue + if (this.state.isRebaseInProgress) { + return { command: 'git.commit', title, tooltip, arguments: [this.repository.sourceControl, commandArg] }; + } + + // Commit + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); const postCommitCommand = config.get<string>('postCommitCommand'); // Branch protection @@ -133,49 +153,36 @@ export class ActionButtonCommand { break; } default: { - commandArg = ''; - title = localize('scm button commit title', "{0} Commit", icon ?? '$(check)'); if (alwaysCommitToNewBranch) { tooltip = this.state.isCommitInProgress ? localize('scm button committing to new branch tooltip', "Committing Changes to New Branch...") : localize('scm button commit to new branch tooltip', "Commit Changes to New Branch"); - } else { - tooltip = this.state.isCommitInProgress ? - localize('scm button committing tooltip', "Committing Changes...") : - localize('scm button commit tooltip', "Commit Changes"); } break; } } - return { - command: { - command: 'git.commit', - title: title, - tooltip: tooltip, - arguments: [this.repository.sourceControl, commandArg], - }, - secondaryCommands: this.getCommitActionButtonSecondaryCommands(), - enabled: this.state.repositoryHasChangesToCommit && !this.state.isCommitInProgress && !this.state.isMergeInProgress - }; + return { command: 'git.commit', title, tooltip, arguments: [this.repository.sourceControl, commandArg] }; } private getCommitActionButtonSecondaryCommands(): Command[][] { const commandGroups: Command[][] = []; - for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) { - const commands = provider.getCommands(new ApiRepository(this.repository)); - commandGroups.push((commands ?? []).map(c => { - return { - command: 'git.commit', - title: c.title, - arguments: [this.repository.sourceControl, c.command] - }; - })); - } + if (!this.state.isRebaseInProgress) { + for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) { + const commands = provider.getCommands(new ApiRepository(this.repository)); + commandGroups.push((commands ?? []).map(c => { + return { + command: 'git.commit', + title: c.title, + arguments: [this.repository.sourceControl, c.command] + }; + })); + } - if (commandGroups.length > 0) { - commandGroups[0].splice(0, 0, { command: 'git.commit', title: localize('scm secondary button commit', "Commit") }); + if (commandGroups.length > 0) { + commandGroups[0].splice(0, 0, { command: 'git.commit', title: localize('scm secondary button commit', "Commit") }); + } } return commandGroups; @@ -185,8 +192,8 @@ export class ActionButtonCommand { const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true }); - // Branch does have an upstream, commit/merge is in progress, or the button is disabled - if (this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || !showActionButton.publish) { return undefined; } + // Branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled + if (this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.publish) { return undefined; } return { command: { @@ -206,8 +213,8 @@ export class ActionButtonCommand { const showActionButton = config.get<{ sync: boolean }>('showActionButton', { sync: true }); const branchIsAheadOrBehind = (this.state.HEAD?.behind ?? 0) > 0 || (this.state.HEAD?.ahead ?? 0) > 0; - // Branch does not have an upstream, branch is not ahead/behind the remote branch, commit/merge is in progress, or the button is disabled - if (!this.state.HEAD?.upstream || !branchIsAheadOrBehind || this.state.isCommitInProgress || this.state.isMergeInProgress || !showActionButton.sync) { return undefined; } + // Branch does not have an upstream, branch is not ahead/behind the remote branch, commit/merge/rebase is in progress, or the button is disabled + if (!this.state.HEAD?.upstream || !branchIsAheadOrBehind || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.sync) { return undefined; } const ahead = this.state.HEAD.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : ''; const behind = this.state.HEAD.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : ''; @@ -229,7 +236,8 @@ export class ActionButtonCommand { private onDidChangeOperations(): void { const isCommitInProgress = - this.repository.operations.isRunning(Operation.Commit); + this.repository.operations.isRunning(Operation.Commit) || + this.repository.operations.isRunning(Operation.RebaseContinue); const isSyncInProgress = this.repository.operations.isRunning(Operation.Sync) || @@ -251,6 +259,7 @@ export class ActionButtonCommand { ...this.state, HEAD: this.repository.HEAD, isMergeInProgress: this.repository.mergeGroup.resourceStates.length !== 0, + isRebaseInProgress: !!this.repository.rebaseCommit, repositoryHasChangesToCommit: this.repositoryHasChangesToCommit() }; } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index e0b7b5d4670..560734f6316 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -432,11 +432,11 @@ export class CommandCenter { ]); // ours (current branch and commit) ours.detail = head.refNames.map(s => s.replace(/^HEAD ->/, '')).join(', '); - ours.description = head.hash.substring(0, 7); + ours.description = '$(git-commit) ' + head.hash.substring(0, 7); // theirs theirs.detail = rebaseOrMergeHead.refNames.join(', '); - theirs.description = rebaseOrMergeHead.hash.substring(0, 7); + theirs.description = '$(git-commit) ' + rebaseOrMergeHead.hash.substring(0, 7); } catch (error) { // not so bad, can continue with just uris @@ -469,7 +469,7 @@ export class CommandCenter { /* __GDPR__ "clone" : { "owner": "lszomoru", - "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" } } */ this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' }); @@ -495,7 +495,7 @@ export class CommandCenter { /* __GDPR__ "clone" : { "owner": "lszomoru", - "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" } } */ this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' }); @@ -554,8 +554,8 @@ export class CommandCenter { /* __GDPR__ "clone" : { "owner": "lszomoru", - "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } + "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }, + "openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Indicates whether the folder is opened following the clone operation" } } */ this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: action === PostCloneAction.Open || action === PostCloneAction.OpenNewWindow ? 1 : 0 }); @@ -574,7 +574,7 @@ export class CommandCenter { /* __GDPR__ "clone" : { "owner": "lszomoru", - "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" } } */ this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' }); @@ -584,7 +584,7 @@ export class CommandCenter { /* __GDPR__ "clone" : { "owner": "lszomoru", - "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" } } */ this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' }); @@ -1524,7 +1524,7 @@ export class CommandCenter { } if (opts.all === undefined) { - opts = { all: noStagedChanges }; + opts = { ...opts, all: noStagedChanges }; } else if (!opts.all && noStagedChanges && !opts.empty) { opts = { ...opts, all: true }; } @@ -3066,7 +3066,7 @@ export class CommandCenter { /* __GDPR__ "git.command" : { "owner": "lszomoru", - "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The command id of the command being executed" } } */ this.telemetryReporter.sendTelemetryEvent('git.command', { command: id }); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 0ccf22301c4..92e16934334 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1475,7 +1475,7 @@ export class Repository { const args = ['rebase', '--continue']; try { - await this.exec(args); + await this.exec(args, { env: { GIT_EDITOR: 'true' } }); } catch (commitErr) { await this.handleCommitError(commitErr); } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 995f9579e99..2cbddcc0caf 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -617,7 +617,7 @@ class ResourceCommandResolver { if (!resource.leftUri) { const bothModified = resource.type === Status.BOTH_MODIFIED; - if (resource.rightUri && bothModified && workspace.getConfiguration('git').get<boolean>('mergeEditor', false)) { + if (resource.rightUri && workspace.getConfiguration('git').get<boolean>('mergeEditor', false) && (bothModified || resource.type === Status.BOTH_ADDED)) { return { command: '_git.openMergeEditor', title: localize('open.merge', "Open Merge"), @@ -1894,9 +1894,9 @@ export class Repository implements Disposable { /* __GDPR__ "statusLimit" : { "owner": "lszomoru", - "ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "limit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + "ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting indicating whether submodules are ignored" }, + "limit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Setting indicating the limit of status entries" }, + "statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of status entries" } } */ this.telemetryReporter.sendTelemetryEvent('statusLimit', { ignoreSubmodules: String(ignoreSubmodules) }, { limit, statusLength }); @@ -2028,6 +2028,9 @@ export class Repository implements Disposable { // set count badge this.setCountBadge(); + // set mergeChanges context + commands.executeCommand('setContext', 'git.mergeChanges', merge.map(item => item.resourceUri.toString())); + this._onDidChangeStatus.fire(); this._sourceControl.commitTemplate = await this.getInputTemplate(); diff --git a/extensions/handlebars/package.json b/extensions/handlebars/package.json index 0eb6cb2eb2b..88976cf36ff 100644 --- a/extensions/handlebars/package.json +++ b/extensions/handlebars/package.json @@ -36,6 +36,12 @@ "scopeName": "text.html.handlebars", "path": "./syntaxes/Handlebars.tmLanguage.json" } + ], + "htmlLanguageParticipants": [ + { + "languageId": "handlebars", + "autoInsert": true + } ] }, "repository": { diff --git a/extensions/html-language-features/client/src/autoInsertion.ts b/extensions/html-language-features/client/src/autoInsertion.ts index 170afa46c02..e95e6a64a09 100644 --- a/extensions/html-language-features/client/src/autoInsertion.ts +++ b/extensions/html-language-features/client/src/autoInsertion.ts @@ -5,8 +5,9 @@ import { window, workspace, Disposable, TextDocument, Position, SnippetString, TextDocumentChangeEvent, TextDocumentChangeReason, TextDocumentContentChangeEvent } from 'vscode'; import { Runtime } from './htmlClient'; +import { LanguageParticipants } from './languageParticipants'; -export function activateAutoInsertion(provider: (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position) => Thenable<string>, supportedLanguages: { [id: string]: boolean }, runtime: Runtime): Disposable { +export function activateAutoInsertion(provider: (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position) => Thenable<string>, languageParticipants: LanguageParticipants, runtime: Runtime): Disposable { const disposables: Disposable[] = []; workspace.onDidChangeTextDocument(onDidChangeTextDocument, null, disposables); @@ -33,7 +34,7 @@ export function activateAutoInsertion(provider: (kind: 'autoQuote' | 'autoClose' return; } const document = editor.document; - if (!supportedLanguages[document.languageId]) { + if (!languageParticipants.useAutoInsert(document.languageId)) { return; } const configurations = workspace.getConfiguration(undefined, document.uri); diff --git a/extensions/html-language-features/client/src/browser/htmlClientMain.ts b/extensions/html-language-features/client/src/browser/htmlClientMain.ts index ab23520fe79..fb8cc9071a8 100644 --- a/extensions/html-language-features/client/src/browser/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/browser/htmlClientMain.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, ExtensionContext, Uri } from 'vscode'; -import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; -import { startClient, LanguageClientConstructor } from '../htmlClient'; +import { LanguageClientOptions } from 'vscode-languageclient'; +import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient'; import { LanguageClient } from 'vscode-languageclient/browser'; declare const Worker: { @@ -15,7 +15,7 @@ declare const TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string }; }; -let client: BaseLanguageClient | undefined; +let client: AsyncDisposable | undefined; // this method is called when vs code is activated export async function activate(context: ExtensionContext) { @@ -42,7 +42,7 @@ export async function activate(context: ExtensionContext) { export async function deactivate(): Promise<void> { if (client) { - await client.stop(); + await client.dispose(); client = undefined; } } diff --git a/extensions/html-language-features/client/src/customData.ts b/extensions/html-language-features/client/src/customData.ts index 80e5f2f04d9..71d847a4f39 100644 --- a/extensions/html-language-features/client/src/customData.ts +++ b/extensions/html-language-features/client/src/customData.ts @@ -125,7 +125,7 @@ function collectInWorkspaces(workspaceUris: Set<string>): Set<string> { } function collectInExtensions(localExtensionUris: Set<string>, externalUris: Set<string>): void { - for (const extension of extensions.all) { + for (const extension of extensions.allAcrossExtensionHosts) { const customData = extension.packageJSON?.contributes?.html?.customData; if (Array.isArray(customData)) { for (const uriOrPath of customData) { diff --git a/extensions/html-language-features/client/src/htmlClient.ts b/extensions/html-language-features/client/src/htmlClient.ts index 60accb3870b..d5cf527713f 100644 --- a/extensions/html-language-features/client/src/htmlClient.ts +++ b/extensions/html-language-features/client/src/htmlClient.ts @@ -9,7 +9,7 @@ const localize = nls.loadMessageBundle(); import { languages, ExtensionContext, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, extensions, Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList, SemanticTokensLegend, - DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands + DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands, OutputChannel } from 'vscode'; import { LanguageClientOptions, RequestType, DocumentRangeFormattingParams, @@ -18,6 +18,7 @@ import { import { FileSystemProvider, serveFileSystemRequests } from './requests'; import { getCustomDataSource } from './customData'; import { activateAutoInsertion } from './autoInsertion'; +import { getLanguageParticipants, LanguageParticipants } from './languageParticipants'; namespace CustomDataChangedNotification { export const type: NotificationType<string[]> = new NotificationType('html/customDataChanged'); @@ -74,6 +75,8 @@ export interface TelemetryReporter { export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient; +export const languageServerDescription = localize('htmlserver.name', 'HTML Language Server'); + export interface Runtime { TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string } }; fileFs?: FileSystemProvider; @@ -83,11 +86,69 @@ export interface Runtime { }; } -export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<BaseLanguageClient> { +export interface AsyncDisposable { + dispose(): Promise<void>; +} + +export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> { + + const outputChannel = window.createOutputChannel(languageServerDescription); + + const languageParticipants = getLanguageParticipants(); + context.subscriptions.push(languageParticipants); + + let client: Disposable | undefined = await startClientWithParticipants(languageParticipants, newLanguageClient, outputChannel, runtime); + + const promptForLinkedEditingKey = 'html.promptForLinkedEditing'; + if (extensions.getExtension('formulahendry.auto-rename-tag') !== undefined && (context.globalState.get(promptForLinkedEditingKey) !== false)) { + const config = workspace.getConfiguration('editor', { languageId: 'html' }); + if (!config.get('linkedEditing') && !config.get('renameOnType')) { + const activeEditorListener = window.onDidChangeActiveTextEditor(async e => { + if (e && languageParticipants.hasLanguage(e.document.languageId)) { + context.globalState.update(promptForLinkedEditingKey, false); + activeEditorListener.dispose(); + const configure = localize('configureButton', 'Configure'); + const res = await window.showInformationMessage(localize('linkedEditingQuestion', 'VS Code now has built-in support for auto-renaming tags. Do you want to enable it?'), configure); + if (res === configure) { + commands.executeCommand('workbench.action.openSettings', SettingIds.linkedEditing); + } + } + }); + context.subscriptions.push(activeEditorListener); + } + } + + let restartTrigger: Disposable | undefined; + languageParticipants.onDidChange(() => { + if (restartTrigger) { + restartTrigger.dispose(); + } + restartTrigger = runtime.timer.setTimeout(async () => { + if (client) { + outputChannel.appendLine('Extensions have changed, restarting HTML server...'); + outputChannel.appendLine(''); + const oldClient = client; + client = undefined; + await oldClient.dispose(); + client = await startClientWithParticipants(languageParticipants, newLanguageClient, outputChannel, runtime); + } + }, 2000); + }); + + return { + dispose: async () => { + restartTrigger?.dispose(); + await client?.dispose(); + outputChannel.dispose(); + } + }; +} + +async function startClientWithParticipants(languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, outputChannel: OutputChannel, runtime: Runtime): Promise<AsyncDisposable> { - const toDispose = context.subscriptions; + const toDispose: Disposable[] = []; - const documentSelector = ['html', 'handlebars']; + const documentSelector = languageParticipants.documentSelector; const embeddedLanguages = { css: true, javascript: true }; let rangeFormatting: Disposable | undefined = undefined; @@ -129,22 +190,23 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } } }; + clientOptions.outputChannel = outputChannel; // Create the language client and start the client. - const client = newLanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), clientOptions); + const client = newLanguageClient('html', languageServerDescription, clientOptions); client.registerProposedFeatures(); await client.start(); toDispose.push(serveFileSystemRequests(client, runtime)); - const customDataSource = getCustomDataSource(runtime, context.subscriptions); + const customDataSource = getCustomDataSource(runtime, toDispose); client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); customDataSource.onDidChange(() => { client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); - }); - client.onRequest(CustomDataContent.type, customDataSource.getContent); + }, undefined, toDispose); + toDispose.push(client.onRequest(CustomDataContent.type, customDataSource.getContent)); const insertRequestor = (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position): Promise<string> => { @@ -155,7 +217,8 @@ export async function startClient(context: ExtensionContext, newLanguageClient: }; return client.sendRequest(AutoInsertRequest.type, param); }; - const disposable = activateAutoInsertion(insertRequestor, { html: true, handlebars: true }, runtime); + + const disposable = activateAutoInsertion(insertRequestor, languageParticipants, runtime); toDispose.push(disposable); const disposable2 = client.onTelemetry(e => { @@ -193,7 +256,6 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } }); - function updateFormatterRegistration() { const formatEnabled = workspace.getConfiguration().get(SettingIds.formatEnable); if (!formatEnabled && rangeFormatting) { @@ -278,25 +340,12 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } })); - const promptForLinkedEditingKey = 'html.promptForLinkedEditing'; - if (extensions.getExtension('formulahendry.auto-rename-tag') !== undefined && (context.globalState.get(promptForLinkedEditingKey) !== false)) { - const config = workspace.getConfiguration('editor', { languageId: 'html' }); - if (!config.get('linkedEditing') && !config.get('renameOnType')) { - const activeEditorListener = window.onDidChangeActiveTextEditor(async e => { - if (e && documentSelector.indexOf(e.document.languageId) !== -1) { - context.globalState.update(promptForLinkedEditingKey, false); - activeEditorListener.dispose(); - const configure = localize('configureButton', 'Configure'); - const res = await window.showInformationMessage(localize('linkedEditingQuestion', 'VS Code now has built-in support for auto-renaming tags. Do you want to enable it?'), configure); - if (res === configure) { - commands.executeCommand('workbench.action.openSettings', SettingIds.linkedEditing); - } - } - }); - toDispose.push(activeEditorListener); + return { + dispose: async () => { + await client.stop(); + toDispose.forEach(d => d.dispose()); + rangeFormatting?.dispose(); } - } - - return client; + }; } diff --git a/extensions/html-language-features/client/src/languageParticipants.ts b/extensions/html-language-features/client/src/languageParticipants.ts new file mode 100644 index 00000000000..6abe49237c8 --- /dev/null +++ b/extensions/html-language-features/client/src/languageParticipants.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DocumentSelector } from 'vscode-languageclient'; +import { Event, EventEmitter, extensions } from 'vscode'; + +/** + * HTML language participant contribution. + */ +interface LanguageParticipantContribution { + /** + * The id of the language which participates with the HTML language server. + */ + languageId: string; + /** + * true if the language activates the auto insertion and false otherwise. + */ + autoInsert?: boolean; +} + +export interface LanguageParticipants { + readonly onDidChange: Event<void>; + readonly documentSelector: DocumentSelector; + hasLanguage(languageId: string): boolean; + useAutoInsert(languageId: string): boolean; + dispose(): void; +} + +export function getLanguageParticipants(): LanguageParticipants { + const onDidChangeEmmiter = new EventEmitter<void>(); + let languages = new Set<string>(); + let autoInsert = new Set<string>(); + + function update() { + const oldLanguages = languages, oldAutoInsert = autoInsert; + + languages = new Set(); + languages.add('html'); + autoInsert = new Set(); + autoInsert.add('html'); + + for (const extension of extensions.allAcrossExtensionHosts) { + const htmlLanguageParticipants = extension.packageJSON?.contributes?.htmlLanguageParticipants as LanguageParticipantContribution[]; + if (Array.isArray(htmlLanguageParticipants)) { + for (const htmlLanguageParticipant of htmlLanguageParticipants) { + const languageId = htmlLanguageParticipant.languageId; + if (typeof languageId === 'string') { + languages.add(languageId); + if (htmlLanguageParticipant.autoInsert !== false) { + autoInsert.add(languageId); + } + } + } + } + } + return !isEqualSet(languages, oldLanguages) || !isEqualSet(oldLanguages, oldAutoInsert); + } + update(); + + const changeListener = extensions.onDidChange(_ => { + if (update()) { + onDidChangeEmmiter.fire(); + } + }); + + return { + onDidChange: onDidChangeEmmiter.event, + get documentSelector() { return Array.from(languages); }, + hasLanguage(languageId: string) { return languages.has(languageId); }, + useAutoInsert(languageId: string) { return autoInsert.has(languageId); }, + dispose: () => changeListener.dispose() + }; +} + +function isEqualSet<T>(s1: Set<T>, s2: Set<T>) { + if (s1.size !== s2.size) { + return false; + } + for (const e of s1) { + if (!s2.has(e)) { + return false; + } + } + return true; +} diff --git a/extensions/html-language-features/client/src/node/htmlClientMain.ts b/extensions/html-language-features/client/src/node/htmlClientMain.ts index 11ca6f254f9..c3e8c85cf4f 100644 --- a/extensions/html-language-features/client/src/node/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/node/htmlClientMain.ts @@ -5,15 +5,15 @@ import { getNodeFileFS } from './nodeFs'; import { Disposable, ExtensionContext } from 'vscode'; -import { startClient, LanguageClientConstructor } from '../htmlClient'; -import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node'; +import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient'; +import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; import { TextDecoder } from 'util'; import * as fs from 'fs'; import TelemetryReporter from '@vscode/extension-telemetry'; let telemetry: TelemetryReporter | undefined; -let client: BaseLanguageClient | undefined; +let client: AsyncDisposable | undefined; // this method is called when vs code is activated export async function activate(context: ExtensionContext) { @@ -50,7 +50,7 @@ export async function activate(context: ExtensionContext) { export async function deactivate(): Promise<void> { if (client) { - await client.stop(); + await client.dispose(); client = undefined; } } diff --git a/extensions/html-language-features/client/tsconfig.json b/extensions/html-language-features/client/tsconfig.json index 573b24b4aa6..8f5cef74fd3 100644 --- a/extensions/html-language-features/client/tsconfig.json +++ b/extensions/html-language-features/client/tsconfig.json @@ -5,6 +5,7 @@ }, "include": [ "src/**/*", - "../../../src/vscode-dts/vscode.d.ts" + "../../../src/vscode-dts/vscode.d.ts", + "../../../src/vscode-dts/vscode.proposed.extensionsAny.d.ts" ] } diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index b3731cd0acd..7e4120eeb05 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -14,6 +14,9 @@ "onLanguage:html", "onLanguage:handlebars" ], + "enabledApiProposals": [ + "extensionsAny" + ], "main": "./client/out/node/htmlClientMain", "browser": "./client/dist/browser/htmlClientMain", "capabilities": { diff --git a/extensions/html-language-features/schemas/package.schema.json b/extensions/html-language-features/schemas/package.schema.json index a4d8715b918..ef717dbd1d1 100644 --- a/extensions/html-language-features/schemas/package.schema.json +++ b/extensions/html-language-features/schemas/package.schema.json @@ -13,6 +13,23 @@ "type": "string", "description": "Relative path to a HTML custom data file" } + }, + "htmlLanguageParticipants": { + "type": "array", + "description": "A list of languages that participate with the HTML language server.", + "items": { + "type": "object", + "properties": { + "languageId": { + "type": "string", + "description": "The id of the language that participates with HTML language server." + }, + "autoInsert": { + "type": "boolean", + "description": "Whether the language participates with HTML auto insertions. If not specified, defaults to <code>true</code>." + } + } + } } } } diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 322f6b81f85..fc9b3634c9a 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -10,7 +10,7 @@ "main": "./out/node/htmlServerMain", "dependencies": { "vscode-css-languageservice": "^6.0.1", - "vscode-html-languageservice": "^5.0.0", + "vscode-html-languageservice": "^5.0.1", "vscode-languageserver": "^8.0.2-next.4", "vscode-languageserver-textdocument": "^1.0.4", "vscode-nls": "^5.0.1", diff --git a/extensions/html-language-features/server/src/utils/documentContext.ts b/extensions/html-language-features/server/src/utils/documentContext.ts index fb1a6622674..88e3f032885 100644 --- a/extensions/html-language-features/server/src/utils/documentContext.ts +++ b/extensions/html-language-features/server/src/utils/documentContext.ts @@ -24,6 +24,10 @@ export function getDocumentContext(documentUri: string, workspaceFolders: Worksp return { resolveReference: (ref: string, base = documentUri) => { + if (ref.match(/^\w[\w\d+.-]*:/)) { + // starts with a schema + return ref; + } if (ref[0] === '/') { // resolve absolute path against the current workspace folder const folderUri = getRootFolder(); if (folderUri) { diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index 6f3a8b7b5b1..7f6ae130b3f 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -22,10 +22,10 @@ vscode-css-languageservice@^6.0.1: vscode-nls "^5.0.1" vscode-uri "^3.0.3" -vscode-html-languageservice@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.0.0.tgz#c68613f836d7fcff125183d78e6f1f0ff326fa55" - integrity sha512-KJG13z54aLszskp3ETf8b1EKDypr2Sf5RUsfR6OXmKqEl2ZUfyIxsWz4gbJWjPzoJZx/bGH0ZXVwxJ1rg8OKRQ== +vscode-html-languageservice@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.0.1.tgz#bdf7847d27a453a9e98ae2836ead7594784c5c1c" + integrity sha512-OYsyn5HGAhxs0OIG+M0jc34WnftLtD67Wg7+TfrYwvf0waOkkr13zUqtdrVm2JPNQ6fJx+qnuM+vTbq7o1dCdQ== dependencies: vscode-languageserver-textdocument "^1.0.4" vscode-languageserver-types "^3.17.1" diff --git a/extensions/ipynb/esbuild.js b/extensions/ipynb/esbuild.js new file mode 100644 index 00000000000..444ec268030 --- /dev/null +++ b/extensions/ipynb/esbuild.js @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +//@ts-check + +const path = require('path'); +const fse = require('fs-extra'); +const esbuild = require('esbuild'); + +const args = process.argv.slice(2); + +const isWatch = args.indexOf('--watch') >= 0; + +let outputRoot = __dirname; +const outputRootIndex = args.indexOf('--outputRoot'); +if (outputRootIndex >= 0) { + outputRoot = args[outputRootIndex + 1]; +} + +const srcDir = path.join(__dirname, 'src'); +const outDir = path.join(outputRoot, 'out'); + +async function build() { + await esbuild.build({ + entryPoints: [ + path.join(srcDir, 'cellAttachmentRenderer.ts'), + ], + bundle: true, + minify: false, + sourcemap: false, + format: 'esm', + outdir: outDir, + platform: 'browser', + target: ['es2020'], + }); +} + + +build().catch(() => process.exit(1)); + +if (isWatch) { + const watcher = require('@parcel/watcher'); + watcher.subscribe(srcDir, () => { + return build(); + }); +} diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index 1e251a5a19b..7e057f605b7 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -51,6 +51,16 @@ "priority": "default" } ], + "notebookRenderer": [ + { + "id": "vscode.markdown-it-cell-attachment-renderer", + "displayName": "Markdown it ipynb Cell Attachment renderer", + "entrypoint": { + "extends": "vscode.markdown-it-renderer", + "path": "./out/cellAttachmentRenderer.js" + } + } + ], "menus": { "file/newFile": [ { @@ -70,8 +80,9 @@ } }, "scripts": { - "compile": "npx gulp compile-extension:ipynb", - "watch": "npx gulp watch-extension:ipynb" + "compile": "npx gulp compile-extension:ipynb && npm run build-notebook", + "watch": "npx gulp watch-extension:ipynb", + "build-notebook": "node ./esbuild" }, "dependencies": { "@enonic/fnv-plus": "^1.3.0", @@ -80,6 +91,7 @@ }, "devDependencies": { "@jupyterlab/nbformat": "^3.2.9", + "@types/markdown-it": "12.2.3", "@types/uuid": "^8.3.1" }, "repository": { diff --git a/extensions/ipynb/src/cellAttachmentRenderer.ts b/extensions/ipynb/src/cellAttachmentRenderer.ts new file mode 100644 index 00000000000..7e05834dbcb --- /dev/null +++ b/extensions/ipynb/src/cellAttachmentRenderer.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as MarkdownIt from 'markdown-it'; +import type * as MarkdownItToken from 'markdown-it/lib/token'; +import type { RendererContext } from 'vscode-notebook-renderer'; + +interface MarkdownItRenderer { + extendMarkdownIt(fn: (md: MarkdownIt) => void): void; +} + +export async function activate(ctx: RendererContext<void>) { + const markdownItRenderer = (await ctx.getRenderer('vscode.markdown-it-renderer')) as MarkdownItRenderer | any; + if (!markdownItRenderer) { + throw new Error(`Could not load 'vscode.markdown-it-renderer'`); + } + + markdownItRenderer.extendMarkdownIt((md: MarkdownIt) => { + const original = md.renderer.rules.image; + md.renderer.rules.image = (tokens: MarkdownItToken[], idx: number, options, env, self) => { + const token = tokens[idx]; + const src = token.attrGet('src'); + const attachments: Record<string, Record<string, string>> = env.outputItem.metadata?.custom?.attachments; + if (attachments && src) { + const [attachmentKey, attachmentVal] = Object.entries(attachments[src.replace('attachment:', '')])[0]; + const b64Markdown = 'data:' + attachmentKey + ';base64,' + attachmentVal; + token.attrSet('src', b64Markdown); + } + + if (original) { + return original(tokens, idx, options, env, self); + } else { + return self.renderToken(tokens, idx, options); + } + }; + }); +} diff --git a/extensions/ipynb/yarn.lock b/extensions/ipynb/yarn.lock index c932570ecc0..7b5488e7109 100644 --- a/extensions/ipynb/yarn.lock +++ b/extensions/ipynb/yarn.lock @@ -8,21 +8,39 @@ integrity sha512-BCN9uNWH8AmiP7BXBJqEinUY9KXalmRzo+L0cB/mQsmFfzODxwQrbvxCHXUNH2iP+qKkWYtB4vyy8N62PViMFw== "@jupyterlab/nbformat@^3.2.9": - version "3.2.9" - resolved "https://registry.yarnpkg.com/@jupyterlab/nbformat/-/nbformat-3.2.9.tgz#e7d854719612133498af4280d9a8caa0873205b0" - integrity sha512-WSf9OQo8yfFjyodbXRdFoaNwMkaAL5jFZiD6V2f8HqI380ipansWrrV7R9CGzPfgKHpUGZMO1tYKmUwzMhvZ4w== + version "3.4.3" + resolved "https://registry.yarnpkg.com/@jupyterlab/nbformat/-/nbformat-3.4.3.tgz#cbab1bf507677b7f0f309d8353fc83fe5a973c82" + integrity sha512-i/yADrwhhAJJCUOTa+fEBMyJO7fvX9Y73I0B7V6dQhGcrmrEKLC3wk4yOo63+jRntd5+dupbiOtz3w1ncIXwIA== dependencies: - "@lumino/coreutils" "^1.5.3" + "@lumino/coreutils" "^1.11.0" -"@lumino/coreutils@^1.5.3": +"@lumino/coreutils@^1.11.0": version "1.12.0" resolved "https://registry.yarnpkg.com/@lumino/coreutils/-/coreutils-1.12.0.tgz#fbdef760f736eaf2bd396a5c6fc3a68a4b449b15" integrity sha512-DSglh4ylmLi820CNx9soJmDJCpUgymckdWeGWuN0Ash5g60oQvrQDfosVxEhzmNvtvXv45WZEqSBzDP6E5SEmQ== +"@types/linkify-it@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" + integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== + +"@types/markdown-it@12.2.3": + version "12.2.3" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" + integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ== + dependencies: + "@types/linkify-it" "*" + "@types/mdurl" "*" + +"@types/mdurl@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" + integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== + "@types/uuid@^8.3.1": - version "8.3.1" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" - integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== detect-indent@^6.0.0: version "6.1.0" diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 0b5d9f8358e..952abb1a463 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -398,16 +398,27 @@ "description": "%configuration.markdown.suggest.paths.enabled.description%", "scope": "resource" }, - "markdown.trace": { + "markdown.trace.extension": { "type": "string", "enum": [ "off", "verbose" ], "default": "off", - "description": "%markdown.trace.desc%", + "description": "%markdown.trace.extension.desc%", "scope": "window" }, + "markdown.trace.server": { + "type": "string", + "scope": "window", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "description": "%markdown.trace.server.desc%" + }, "markdown.editor.drop.enabled": { "type": "boolean", "default": true, @@ -478,6 +489,7 @@ "type": "string", "scope": "resource", "markdownDescription": "%configuration.markdown.experimental.validate.fileLinks.markdownFragmentLinks.description%", + "default": "ignore", "enum": [ "ignore", "warning", @@ -563,7 +575,8 @@ "@types/vscode-notebook-renderer": "^1.60.0", "@types/vscode-webview": "^1.57.0", "lodash.throttle": "^4.1.1", - "vscode-languageserver-types": "^3.17.2" + "vscode-languageserver-types": "^3.17.2", + "vscode-markdown-languageservice": "^0.0.0-alpha.10" }, "repository": { "type": "git", diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index 7b815072397..062c6ac899f 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -17,7 +17,8 @@ "markdown.showSource.title": "Show Source", "markdown.styles.dec": "A list of URLs or local paths to CSS style sheets to use from the Markdown preview. Relative paths are interpreted relative to the folder open in the Explorer. If there is no open folder, they are interpreted relative to the location of the Markdown file. All '\\' need to be written as '\\\\'.", "markdown.showPreviewSecuritySelector.title": "Change Preview Security Settings", - "markdown.trace.desc": "Enable debug logging for the Markdown extension.", + "markdown.trace.extension.desc": "Enable debug logging for the Markdown extension.", + "markdown.trace.server.desc": "Traces the communication between VS Code and the Markdown language server.", "markdown.preview.refresh.title": "Refresh Preview", "markdown.preview.toggleLock.title": "Toggle Preview Locking", "markdown.findAllFileReferences": "Find File References", diff --git a/extensions/markdown-language-features/server/.npmignore b/extensions/markdown-language-features/server/.npmignore new file mode 100644 index 00000000000..bfd4215998c --- /dev/null +++ b/extensions/markdown-language-features/server/.npmignore @@ -0,0 +1,12 @@ +.vscode/ +.github/ +out/test/ +src/ +.eslintrc.js +.gitignore +tsconfig*.json +*.tsbuildinfo +*.map +example.cjs +CODE_OF_CONDUCT.md +SECURITY.md
\ No newline at end of file diff --git a/extensions/markdown-language-features/server/.vscode/launch.json b/extensions/markdown-language-features/server/.vscode/launch.json index a28c18b6973..fd9033bffaa 100644 --- a/extensions/markdown-language-features/server/.vscode/launch.json +++ b/extensions/markdown-language-features/server/.vscode/launch.json @@ -6,9 +6,11 @@ "name": "Attach", "type": "node", "request": "attach", - "port": 7692, + "port": 7997, "sourceMaps": true, - "outFiles": ["${workspaceFolder}/out/**/*.js"] + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ] } ] }
\ No newline at end of file diff --git a/extensions/markdown-language-features/server/.vscode/settings.json b/extensions/markdown-language-features/server/.vscode/settings.json new file mode 100644 index 00000000000..7a73a41bfdf --- /dev/null +++ b/extensions/markdown-language-features/server/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +}
\ No newline at end of file diff --git a/extensions/markdown-language-features/server/README.md b/extensions/markdown-language-features/server/README.md new file mode 100644 index 00000000000..de4e33926c3 --- /dev/null +++ b/extensions/markdown-language-features/server/README.md @@ -0,0 +1,120 @@ +# Markdown Language Server + +> **❗ Import** This is still in development. While the language server is being used by VS Code, it has not yet been tested with other clients. + +The Markdown language server powers VS Code's built-in markdown support, providing tools for writing and browsing Markdown files. It runs as a separate executable and implements the [language server protocol](https://microsoft.github.io/language-server-protocol/overview). + +This server uses the [Markdown Language Service](https://github.com/microsoft/vscode-markdown-languageservice) to implement almost all of the language features. You can use that library if you need a library for working with Markdown instead of a full language server. + + +## Server capabilities + +- [Completions](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) for Markdown links. + +- [Folding](https://microsoft.github.io/language-server-protocol/specification#textDocument_foldingRange) of Markdown regions, block elements, and header sections. + +- [Smart selection](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange) for inline elements, block elements, and header sections. + +- [Document Symbols](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol) for quick navigation to headers in a document. + +- [Workspace Symbols](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) for quick navigation to headers in the workspace + +- [Document links](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentLink) for making Markdown links in a document clickable. + +- [Find all references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) to headers and links across all Markdown files in the workspace. + +- [Rename](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename) of headers and links across all Markdown files in the workspace. + +- [Go to definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) from links to headers or link definitions. + +- (experimental) [Pull diagnostics (validation)](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics) for links. + + + +## Client requirements + +### Initialization options + +The client can send the following initialization options to the server: + +- `markdownFileExtensions` Array file extensions that should be considered as Markdown. These should not include the leading `.`. For example: `['md', 'mdown', 'markdown']`. + +### Settings + +Clients may send a `workspace/didChangeConfiguration` notification to notify the server of settings changes. +The server supports the following settings: + +- `markdown` + - `suggest` + - `paths` + - `enabled` — Enable/disable path suggestions. + - `experimental` + - `validate` + - `enabled` — Enable/disable all validation. + - `referenceLinks` + - `enabled` — Enable/disable validation of reference links: `[text][ref]` + - `fragmentLinks` + - `enabled` — Enable/disable validation of links to fragments in the current files: `[text](#head)` + - `fileLinks` + - `enabled` — Enable/disable validation of links to file in the workspace. + - `markdownFragmentLinks` — Enable/disable validation of links to headers in other Markdown files. + - `ignoreLinks` — Array of glob patterns for files that should not be validated. + +### Custom requests + +To support all of the features of the language server, the client needs to implement a few custom request types. The definitions of these request types can be found in [`protocol.ts`](./src/protocol.ts) + +#### `markdown/parse` + +Get the tokens for a Markdown file. Clients are expected to use [Markdown-it](https://github.com/markdown-it/markdown-it) for this. + +We require that clients bring their own version of Markdown-it so that they can customize/extend Markdown-it. + +#### `markdown/fs/readFile` + +Read the contents of a file in the workspace. + +#### `markdown/fs/readDirectory` + +Read the contents of a directory in the workspace. + +#### `markdown/fs/stat` + +Check if a given file/directory exists in the workspace. + +#### `markdown/fs/watcher/create` + +Create a file watcher. This is needed for diagnostics support. + +#### `markdown/fs/watcher/delete` + +Delete a previously created file watcher. + +#### `markdown/findMarkdownFilesInWorkspace` + +Get a list of all markdown files in the workspace. + + +## Contribute + +The source code of the Markdown language server can be found in the [VSCode repository](https://github.com/microsoft/vscode) at [extensions/markdown-language-features/server](https://github.com/microsoft/vscode/tree/master/extensions/markdown-language-features/server). + +File issues and pull requests in the [VSCode GitHub Issues](https://github.com/microsoft/vscode/issues). See the document [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) on how to build and run from source. + +Most of the functionality of the server is located in libraries: + +- [vscode-markdown-languageservice](https://github.com/microsoft/vscode-markdown-languageservice) contains the implementation of all features as a reusable library. +- [vscode-languageserver-node](https://github.com/microsoft/vscode-languageserver-node) contains the implementation of language server for NodeJS. + +Help on any of these projects is very welcome. + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [MIT](https://github.com/microsoft/vscode/blob/master/LICENSE.txt) License. + diff --git a/extensions/markdown-language-features/server/package.json b/extensions/markdown-language-features/server/package.json index 9543321edab..b0d6f254d90 100644 --- a/extensions/markdown-language-features/server/package.json +++ b/extensions/markdown-language-features/server/package.json @@ -1,7 +1,7 @@ { "name": "vscode-markdown-languageserver", "description": "Markdown language server", - "version": "1.0.0", + "version": "0.0.0-alpha-1", "author": "Microsoft Corporation", "license": "MIT", "engines": { @@ -13,7 +13,7 @@ "vscode-languageserver": "^8.0.2-next.5`", "vscode-languageserver-textdocument": "^1.0.5", "vscode-languageserver-types": "^3.17.1", - "vscode-markdown-languageservice": "^0.0.0-alpha.8", + "vscode-markdown-languageservice": "^0.0.0-alpha.11", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/markdown-language-features/server/src/configuration.ts b/extensions/markdown-language-features/server/src/configuration.ts new file mode 100644 index 00000000000..5066b3110a6 --- /dev/null +++ b/extensions/markdown-language-features/server/src/configuration.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Connection, Emitter } from 'vscode-languageserver'; +import { Disposable } from './util/dispose'; + +export type ValidateEnabled = 'ignore' | 'warning' | 'error'; + +interface Settings { + readonly markdown: { + readonly suggest: { + readonly paths: { + readonly enabled: boolean; + }; + }; + + readonly experimental: { + readonly validate: { + readonly enabled: true; + readonly referenceLinks: { + readonly enabled: ValidateEnabled; + }; + readonly fragmentLinks: { + readonly enabled: ValidateEnabled; + }; + readonly fileLinks: { + readonly enabled: ValidateEnabled; + readonly markdownFragmentLinks: ValidateEnabled; + }; + readonly ignoreLinks: readonly string[]; + }; + }; + }; +} + + +export class ConfigurationManager extends Disposable { + + private readonly _onDidChangeConfiguration = this._register(new Emitter<Settings>()); + public readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; + + private _settings?: Settings; + + constructor(connection: Connection) { + super(); + + // The settings have changed. Is send on server activation as well. + this._register(connection.onDidChangeConfiguration((change) => { + this._settings = change.settings; + this._onDidChangeConfiguration.fire(this._settings!); + })); + } + + public getSettings(): Settings | undefined { + return this._settings; + } +}
\ No newline at end of file diff --git a/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts new file mode 100644 index 00000000000..6a51b2e986d --- /dev/null +++ b/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Connection, FullDocumentDiagnosticReport, UnchangedDocumentDiagnosticReport } from 'vscode-languageserver'; +import * as md from 'vscode-markdown-languageservice'; +import { disposeAll } from 'vscode-markdown-languageservice/out/util/dispose'; +import { Disposable } from 'vscode-notebook-renderer/events'; +import { URI } from 'vscode-uri'; +import { ConfigurationManager, ValidateEnabled } from '../configuration'; +import { VsCodeClientWorkspace } from '../workspace'; + +const defaultDiagnosticOptions: md.DiagnosticOptions = { + validateFileLinks: md.DiagnosticLevel.ignore, + validateReferences: md.DiagnosticLevel.ignore, + validateFragmentLinks: md.DiagnosticLevel.ignore, + validateMarkdownFileLinkFragments: md.DiagnosticLevel.ignore, + ignoreLinks: [], +}; + +function convertDiagnosticLevel(enabled: ValidateEnabled): md.DiagnosticLevel | undefined { + switch (enabled) { + case 'error': return md.DiagnosticLevel.error; + case 'warning': return md.DiagnosticLevel.warning; + case 'ignore': return md.DiagnosticLevel.ignore; + default: return md.DiagnosticLevel.ignore; + } +} + +function getDiagnosticsOptions(config: ConfigurationManager): md.DiagnosticOptions { + const settings = config.getSettings(); + if (!settings) { + return defaultDiagnosticOptions; + } + + return { + validateFileLinks: convertDiagnosticLevel(settings.markdown.experimental.validate.fileLinks.enabled), + validateReferences: convertDiagnosticLevel(settings.markdown.experimental.validate.referenceLinks.enabled), + validateFragmentLinks: convertDiagnosticLevel(settings.markdown.experimental.validate.fragmentLinks.enabled), + validateMarkdownFileLinkFragments: convertDiagnosticLevel(settings.markdown.experimental.validate.fileLinks.markdownFragmentLinks), + ignoreLinks: settings.markdown.experimental.validate.ignoreLinks, + }; +} + +export function registerValidateSupport( + connection: Connection, + workspace: VsCodeClientWorkspace, + ls: md.IMdLanguageService, + config: ConfigurationManager, +): Disposable { + let diagnosticOptions: md.DiagnosticOptions = defaultDiagnosticOptions; + function updateDiagnosticsSetting(): void { + diagnosticOptions = getDiagnosticsOptions(config); + } + + + const subs: Disposable[] = []; + const manager = ls.createPullDiagnosticsManager(); + subs.push(manager); + + subs.push(manager.onLinkedToFileChanged(() => { + // TODO: We only need to refresh certain files + connection.languages.diagnostics.refresh(); + })); + + connection.languages.diagnostics.on(async (params, token): Promise<FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport> => { + if (!config.getSettings()?.markdown.experimental.validate.enabled) { + return { kind: 'full', items: [] }; + } + + const document = await workspace.openMarkdownDocument(URI.parse(params.textDocument.uri)); + if (!document) { + return { kind: 'full', items: [] }; + } + + const diagnostics = await manager.computeDiagnostics(document, diagnosticOptions, token); + return { + kind: 'full', + items: diagnostics, + }; + }); + + updateDiagnosticsSetting(); + subs.push(config.onDidChangeConfiguration(() => { + updateDiagnosticsSetting(); + connection.languages.diagnostics.refresh(); + })); + + return { + dispose: () => { + disposeAll(subs); + } + }; +} diff --git a/extensions/markdown-language-features/server/src/logging.ts b/extensions/markdown-language-features/server/src/logging.ts index 2a026caf580..2fc08c25b7a 100644 --- a/extensions/markdown-language-features/server/src/logging.ts +++ b/extensions/markdown-language-features/server/src/logging.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILogger } from 'vscode-markdown-languageservice'; +import { ILogger, LogLevel } from 'vscode-markdown-languageservice'; export class LogFunctionLogger implements ILogger { @@ -31,8 +31,9 @@ export class LogFunctionLogger implements ILogger { private readonly _logFn: typeof console.log ) { } - public verbose(title: string, message: string, data?: any): void { - this.appendLine(`[Verbose ${LogFunctionLogger.now()}] ${title}: ${message}`); + + public log(level: LogLevel, title: string, message: string, data?: any): void { + this.appendLine(`[${level} ${LogFunctionLogger.now()}] ${title}: ${message}`); if (data) { this.appendLine(LogFunctionLogger.data2String(data)); } diff --git a/extensions/markdown-language-features/server/src/protocol.ts b/extensions/markdown-language-features/server/src/protocol.ts index 206e0fbe8c7..4b045dce0d3 100644 --- a/extensions/markdown-language-features/server/src/protocol.ts +++ b/extensions/markdown-language-features/server/src/protocol.ts @@ -4,15 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import { RequestType } from 'vscode-languageserver'; -import * as md from 'vscode-markdown-languageservice'; -import * as lsp from 'vscode-languageserver-types'; +import type * as lsp from 'vscode-languageserver-types'; +import type * as md from 'vscode-markdown-languageservice'; -// From server -export const parseRequestType: RequestType<{ uri: string }, md.Token[], any> = new RequestType('markdown/parse'); -export const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile'); -export const statFileRequestType: RequestType<{ uri: string }, md.FileStat | undefined, any> = new RequestType('markdown/statFile'); -export const readDirectoryRequestType: RequestType<{ uri: string }, [string, md.FileStat][], any> = new RequestType('markdown/readDirectory'); -export const findFilesRequestTypes: RequestType<{}, string[], any> = new RequestType('markdown/findFiles'); +//#region From server +export const parse = new RequestType<{ uri: string }, md.Token[], any>('markdown/parse'); -// To server +export const fs_readFile = new RequestType<{ uri: string }, number[], any>('markdown/fs/readFile'); +export const fs_readDirectory = new RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any>('markdown/fs/readDirectory'); +export const fs_stat = new RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any>('markdown/fs/stat'); + +export const fs_watcher_create = new RequestType<{ id: number; uri: string; options: md.FileWatcherOptions }, void, any>('markdown/fs/watcher/create'); +export const fs_watcher_delete = new RequestType<{ id: number }, void, any>('markdown/fs/watcher/delete'); + +export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('markdown/findMarkdownFilesInWorkspace'); +//#endregion + +//#region To server export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace'); + +export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange'); +//#endregion diff --git a/extensions/markdown-language-features/server/src/server.ts b/extensions/markdown-language-features/server/src/server.ts index b5891712533..63a3d5d4b99 100644 --- a/extensions/markdown-language-features/server/src/server.ts +++ b/extensions/markdown-language-features/server/src/server.ts @@ -7,8 +7,11 @@ import { CancellationToken, Connection, InitializeParams, InitializeResult, Note import { TextDocument } from 'vscode-languageserver-textdocument'; import * as lsp from 'vscode-languageserver-types'; import * as md from 'vscode-markdown-languageservice'; +import { IDisposable } from 'vscode-markdown-languageservice/out/util/dispose'; import { URI } from 'vscode-uri'; import { getLsConfiguration } from './config'; +import { ConfigurationManager } from './configuration'; +import { registerValidateSupport } from './languageFeatures/diagnostics'; import { LogFunctionLogger } from './logging'; import * as protocol from './protocol'; import { VsCodeClientWorkspace } from './workspace'; @@ -17,12 +20,17 @@ export async function startServer(connection: Connection) { const documents = new TextDocuments(TextDocument); const notebooks = new NotebookDocuments(documents); + const configurationManager = new ConfigurationManager(connection); + + let provider: md.IMdLanguageService | undefined; + let workspace: VsCodeClientWorkspace | undefined; + connection.onInitialize((params: InitializeParams): InitializeResult => { const parser = new class implements md.IMdParser { slugifier = md.githubSlugifier; async tokenize(document: md.ITextDocument): Promise<md.Token[]> { - return await connection.sendRequest(protocol.parseRequestType, { uri: document.uri.toString() }); + return await connection.sendRequest(protocol.parse, { uri: document.uri.toString() }); } }; @@ -30,8 +38,8 @@ export async function startServer(connection: Connection) { markdownFileExtensions: params.initializationOptions.markdownFileExtensions, }); - const workspace = new VsCodeClientWorkspace(connection, config, documents, notebooks); const logger = new LogFunctionLogger(connection.console.log.bind(connection.console)); + workspace = new VsCodeClientWorkspace(connection, config, documents, notebooks, logger); provider = md.createLanguageService({ workspace, parser, @@ -39,9 +47,18 @@ export async function startServer(connection: Connection) { markdownFileExtensions: config.markdownFileExtensions, }); + registerCompletionsSupport(connection, documents, provider, configurationManager); + registerValidateSupport(connection, workspace, provider, configurationManager); + workspace.workspaceFolders = (params.workspaceFolders ?? []).map(x => URI.parse(x.uri)); return { capabilities: { + diagnosticProvider: { + documentSelector: null, + identifier: 'markdown', + interFileDependencies: true, + workspaceDiagnostics: false, + }, completionProvider: { triggerCharacters: ['.', '/', '#'] }, definitionProvider: true, documentLinkProvider: { resolveProvider: true }, @@ -61,8 +78,6 @@ export async function startServer(connection: Connection) { }); - let provider: md.IMdLanguageService | undefined; - connection.onDocumentLinks(async (params, token): Promise<lsp.DocumentLink[]> => { try { const document = documents.get(params.textDocument.uri); @@ -129,18 +144,6 @@ export async function startServer(connection: Connection) { return []; }); - connection.onCompletion(async (params, token): Promise<lsp.CompletionItem[]> => { - try { - const document = documents.get(params.textDocument.uri); - if (document) { - return await provider!.getCompletionItems(document, params.position, params.context!, token); - } - } catch (e) { - console.error(e.stack); - } - return []; - }); - connection.onReferences(async (params, token): Promise<lsp.Location[]> => { try { const document = documents.get(params.textDocument.uri); @@ -204,3 +207,46 @@ export async function startServer(connection: Connection) { notebooks.listen(connection); connection.listen(); } + + +function registerCompletionsSupport( + connection: Connection, + documents: TextDocuments<TextDocument>, + ls: md.IMdLanguageService, + config: ConfigurationManager, +): IDisposable { + // let registration: Promise<IDisposable> | undefined; + function update() { + // TODO: client still makes the request in this case. Figure our how to properly unregister. + return; + // const settings = config.getSettings(); + // if (settings?.markdown.suggest.paths.enabled) { + // if (!registration) { + // registration = connection.client.register(CompletionRequest.type); + // } + // } else { + // registration?.then(x => x.dispose()); + // registration = undefined; + // } + } + + connection.onCompletion(async (params, token): Promise<lsp.CompletionItem[]> => { + try { + const settings = config.getSettings(); + if (!settings?.markdown.suggest.paths.enabled) { + return []; + } + + const document = documents.get(params.textDocument.uri); + if (document) { + return await ls.getCompletionItems(document, params.position, params.context!, token); + } + } catch (e) { + console.error(e.stack); + } + return []; + }); + + update(); + return config.onDidChangeConfiguration(() => update()); +} diff --git a/extensions/markdown-language-features/server/src/util/dispose.ts b/extensions/markdown-language-features/server/src/util/dispose.ts new file mode 100644 index 00000000000..eee79003ae9 --- /dev/null +++ b/extensions/markdown-language-features/server/src/util/dispose.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export class MultiDisposeError extends Error { + constructor( + public readonly errors: any[] + ) { + super(`Encountered errors while disposing of store. Errors: [${errors.join(', ')}]`); + } +} + +export function disposeAll(disposables: Iterable<IDisposable>) { + const errors: any[] = []; + + for (const disposable of disposables) { + try { + disposable.dispose(); + } catch (e) { + errors.push(e); + } + } + + if (errors.length === 1) { + throw errors[0]; + } else if (errors.length > 1) { + throw new MultiDisposeError(errors); + } +} + +export interface IDisposable { + dispose(): void; +} + +export abstract class Disposable { + private _isDisposed = false; + + protected _disposables: IDisposable[] = []; + + public dispose(): any { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + disposeAll(this._disposables); + } + + protected _register<T extends IDisposable>(value: T): T { + if (this._isDisposed) { + value.dispose(); + } else { + this._disposables.push(value); + } + return value; + } + + protected get isDisposed() { + return this._isDisposed; + } +} + +export class DisposableStore extends Disposable { + private readonly items = new Set<IDisposable>(); + + public override dispose() { + super.dispose(); + disposeAll(this.items); + this.items.clear(); + } + + public add<T extends IDisposable>(item: T): T { + if (this.isDisposed) { + console.warn('Adding to disposed store. Item will be leaked'); + } + + this.items.add(item); + return item; + } +} diff --git a/extensions/markdown-language-features/server/src/workspace.ts b/extensions/markdown-language-features/server/src/workspace.ts index 5847d0c5505..09147851016 100644 --- a/extensions/markdown-language-features/server/src/workspace.ts +++ b/extensions/markdown-language-features/server/src/workspace.ts @@ -6,7 +6,7 @@ import { Connection, Emitter, FileChangeType, NotebookDocuments, TextDocuments } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import * as md from 'vscode-markdown-languageservice'; -import { ContainingDocumentContext } from 'vscode-markdown-languageservice/out/workspace'; +import { ContainingDocumentContext, FileWatcherOptions, IFileSystemWatcher } from 'vscode-markdown-languageservice/out/workspace'; import { URI } from 'vscode-uri'; import { LsConfiguration } from './config'; import * as protocol from './protocol'; @@ -18,7 +18,7 @@ import { Schemes } from './util/schemes'; declare const TextDecoder: any; -export class VsCodeClientWorkspace implements md.IWorkspace { +export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { private readonly _onDidCreateMarkdownDocument = new Emitter<md.ITextDocument>(); public readonly onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocument.event; @@ -33,11 +33,21 @@ export class VsCodeClientWorkspace implements md.IWorkspace { private readonly _utf8Decoder = new TextDecoder('utf-8'); + private _watcherPool = 0; + private readonly _watchers = new Map<number, { + readonly resource: URI; + readonly options: FileWatcherOptions; + readonly onDidChange: Emitter<URI>; + readonly onDidCreate: Emitter<URI>; + readonly onDidDelete: Emitter<URI>; + }>(); + constructor( private readonly connection: Connection, private readonly config: LsConfiguration, private readonly documents: TextDocuments<TextDocument>, private readonly notebooks: NotebookDocuments<TextDocument>, + private readonly logger: md.ILogger, ) { documents.onDidOpen(e => { this._documentCache.delete(URI.parse(e.document.uri)); @@ -59,6 +69,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace { connection.onDidChangeWatchedFiles(async ({ changes }) => { for (const change of changes) { const resource = URI.parse(change.uri); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: onDidChangeWatchedFiles', `${change.type}: ${resource}`); switch (change.type) { case FileChangeType.Changed: { this._documentCache.delete(resource); @@ -83,6 +94,21 @@ export class VsCodeClientWorkspace implements md.IWorkspace { } } }); + + connection.onRequest(protocol.fs_watcher_onChange, params => { + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: fs_watcher_onChange', `${params.kind}: ${params.uri}`); + + const watcher = this._watchers.get(params.id); + if (!watcher) { + return; + } + + switch (params.kind) { + case 'create': watcher.onDidCreate.fire(URI.parse(params.uri)); return; + case 'change': watcher.onDidChange.fire(URI.parse(params.uri)); return; + case 'delete': watcher.onDidDelete.fire(URI.parse(params.uri)); return; + } + }); } public listen() { @@ -108,7 +134,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace { const limiter = new Limiter<md.ITextDocument | undefined>(maxConcurrent); // Add files on disk - const resources = await this.connection.sendRequest(protocol.findFilesRequestTypes, {}); + const resources = await this.connection.sendRequest(protocol.findMarkdownFilesInWorkspace, {}); const onDiskResults = await Promise.all(resources.map(strResource => { return limiter.queue(async () => { const resource = URI.parse(strResource); @@ -148,13 +174,13 @@ export class VsCodeClientWorkspace implements md.IWorkspace { } try { - const response = await this.connection.sendRequest(protocol.readFileRequestType, { uri: resource.toString() }); + const response = await this.connection.sendRequest(protocol.fs_readFile, { uri: resource.toString() }); // TODO: LSP doesn't seem to handle Array buffers well const bytes = new Uint8Array(response); // We assume that markdown is in UTF-8 const text = this._utf8Decoder.decode(bytes); - const doc = new md.InMemoryDocument(resource, text, 0); + const doc = TextDocument.create(resource.toString(), 'markdown', 0, text); this._documentCache.set(resource, doc); return doc; } catch (e) { @@ -163,14 +189,16 @@ export class VsCodeClientWorkspace implements md.IWorkspace { } async stat(resource: URI): Promise<md.FileStat | undefined> { + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: stat', `${resource}`); if (this._documentCache.has(resource) || this.documents.get(resource.toString())) { return { isDirectory: false }; } - return this.connection.sendRequest(protocol.statFileRequestType, { uri: resource.toString() }); + return this.connection.sendRequest(protocol.fs_stat, { uri: resource.toString() }); } async readDirectory(resource: URI): Promise<[string, md.FileStat][]> { - return this.connection.sendRequest(protocol.readDirectoryRequestType, { uri: resource.toString() }); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: readDir', `${resource}`); + return this.connection.sendRequest(protocol.fs_readDirectory, { uri: resource.toString() }); } getContainingDocument(resource: URI): ContainingDocumentContext | undefined { @@ -186,6 +214,37 @@ export class VsCodeClientWorkspace implements md.IWorkspace { return undefined; } + watchFile(resource: URI, options: FileWatcherOptions): IFileSystemWatcher { + const id = this._watcherPool++; + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: watchFile', `(${id}) ${resource}`); + + const entry = { + resource, + options, + onDidCreate: new Emitter<URI>(), + onDidChange: new Emitter<URI>(), + onDidDelete: new Emitter<URI>(), + }; + this._watchers.set(id, entry); + + this.connection.sendRequest(protocol.fs_watcher_create, { + id, + uri: resource.toString(), + options, + }); + + return { + onDidCreate: entry.onDidCreate.event, + onDidChange: entry.onDidChange.event, + onDidDelete: entry.onDidDelete.event, + dispose: () => { + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: disposeWatcher', `(${id}) ${resource}`); + this.connection.sendRequest(protocol.fs_watcher_delete, { id }); + this._watchers.delete(id); + } + }; + } + private isRelevantMarkdownDocument(doc: TextDocument) { return isMarkdownFile(doc) && URI.parse(doc.uri).scheme !== 'vscode-bulkeditpreview'; } diff --git a/extensions/markdown-language-features/server/yarn.lock b/extensions/markdown-language-features/server/yarn.lock index e930ffa60ed..7a44774a574 100644 --- a/extensions/markdown-language-features/server/yarn.lock +++ b/extensions/markdown-language-features/server/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.43.tgz#555e5a743f76b6b897d47f945305b618525ddbe6" integrity sha512-GqWykok+3uocgfAJM8imbozrqLnPyTrpFlrryURQlw1EesPUCx5XxTiucWDSFF9/NUEXDuD4bnvHm8xfVGWTpQ== +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + vscode-jsonrpc@8.0.2-next.1: version "8.0.2-next.1" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2-next.1.tgz#6bdc39fd194782032e34047eeefce562941259c6" @@ -42,15 +47,22 @@ vscode-languageserver@^8.0.2-next.5`: dependencies: vscode-languageserver-protocol "3.17.2-next.6" -vscode-markdown-languageservice@^0.0.0-alpha.8: - version "0.0.0-alpha.8" - resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.0.0-alpha.8.tgz#05d4f86cf0514fd71479847eef742fcc8cdbe87f" - integrity sha512-si8weZsY4LtyonyZwxpFYk8WucRFiKJisErNTt1HDjUCglSDIZqsMNuMIcz3t0nVNfG0LrpdMFVLGhmET5D71Q== +vscode-markdown-languageservice@^0.0.0-alpha.11: + version "0.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.0.0-alpha.11.tgz#9dcdfc23f9dcaeaca3b2a2c76fc504619eaeb284" + integrity sha512-syFamf99xx+Q9DkA66+8fbSz2A2LJkyOV+nSziGgAzdDPv4jkb7eWF6l+nUteHTGbRLQ+q0tfkj0A7OovueCSQ== dependencies: + picomatch "^2.3.1" vscode-languageserver-textdocument "^1.0.5" vscode-languageserver-types "^3.17.1" + vscode-nls "^5.0.1" vscode-uri "^3.0.3" +vscode-nls@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.1.tgz#ba23fc4d4420d25e7f886c8e83cbdcec47aa48b2" + integrity sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A== + vscode-uri@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" diff --git a/extensions/markdown-language-features/src/client.ts b/extensions/markdown-language-features/src/client.ts index 96b43406961..553859bb541 100644 --- a/extensions/markdown-language-features/src/client.ts +++ b/extensions/markdown-language-features/src/client.ts @@ -3,22 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import Token = require('markdown-it/lib/token'); import * as vscode from 'vscode'; -import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType, RequestType } from 'vscode-languageclient'; +import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType } from 'vscode-languageclient'; import * as nls from 'vscode-nls'; import { IMdParser } from './markdownEngine'; -import { markdownFileExtensions } from './util/file'; +import * as proto from './protocol'; +import { looksLikeMarkdownPath, markdownFileExtensions } from './util/file'; import { IMdWorkspace } from './workspace'; const localize = nls.loadMessageBundle(); -const parseRequestType: RequestType<{ uri: string }, Token[], any> = new RequestType('markdown/parse'); -const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile'); -const statFileRequestType: RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any> = new RequestType('markdown/statFile'); -const readDirectoryRequestType: RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any> = new RequestType('markdown/readDirectory'); -const findFilesRequestTypes: RequestType<{}, string[], any> = new RequestType('markdown/findFiles'); - export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient; @@ -34,7 +28,16 @@ export async function startClient(factory: LanguageClientConstructor, workspace: }, initializationOptions: { markdownFileExtensions, - } + }, + diagnosticPullOptions: { + onChange: true, + onSave: true, + onTabs: true, + match(_documentSelector, resource) { + return looksLikeMarkdownPath(resource); + }, + }, + }; const client = factory('markdown', localize('markdownServer.name', 'Markdown Language Server'), clientOptions); @@ -54,7 +57,7 @@ export async function startClient(factory: LanguageClientConstructor, workspace: }); } - client.onRequest(parseRequestType, async (e) => { + client.onRequest(proto.parse, async (e) => { const uri = vscode.Uri.parse(e.uri); const doc = await workspace.getOrLoadMarkdownDocument(uri); if (doc) { @@ -64,12 +67,12 @@ export async function startClient(factory: LanguageClientConstructor, workspace: } }); - client.onRequest(readFileRequestType, async (e): Promise<number[]> => { + client.onRequest(proto.fs_readFile, async (e): Promise<number[]> => { const uri = vscode.Uri.parse(e.uri); return Array.from(await vscode.workspace.fs.readFile(uri)); }); - client.onRequest(statFileRequestType, async (e): Promise<{ isDirectory: boolean } | undefined> => { + client.onRequest(proto.fs_stat, async (e): Promise<{ isDirectory: boolean } | undefined> => { const uri = vscode.Uri.parse(e.uri); try { const stat = await vscode.workspace.fs.stat(uri); @@ -79,16 +82,32 @@ export async function startClient(factory: LanguageClientConstructor, workspace: } }); - client.onRequest(readDirectoryRequestType, async (e): Promise<[string, { isDirectory: boolean }][]> => { + client.onRequest(proto.fs_readDirectory, async (e): Promise<[string, { isDirectory: boolean }][]> => { const uri = vscode.Uri.parse(e.uri); const result = await vscode.workspace.fs.readDirectory(uri); return result.map(([name, type]) => [name, { isDirectory: type === vscode.FileType.Directory }]); }); - client.onRequest(findFilesRequestTypes, async (): Promise<string[]> => { + client.onRequest(proto.findMarkdownFilesInWorkspace, async (): Promise<string[]> => { return (await vscode.workspace.findFiles(mdFileGlob, '**/node_modules/**')).map(x => x.toString()); }); + const watchers = new Map<number, vscode.FileSystemWatcher>(); + + client.onRequest(proto.fs_watcher_create, async (params): Promise<void> => { + const id = params.id; + const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.parse(params.uri), '*'), params.options.ignoreCreate, params.options.ignoreChange, params.options.ignoreDelete); + watchers.set(id, watcher); + watcher.onDidCreate(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'create' }); }); + watcher.onDidChange(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'change' }); }); + watcher.onDidDelete(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'delete' }); }); + }); + + client.onRequest(proto.fs_watcher_delete, async (params): Promise<void> => { + watchers.get(params.id)?.dispose(); + watchers.delete(params.id); + }); + await client.start(); return client; diff --git a/extensions/markdown-language-features/src/extension.browser.ts b/extensions/markdown-language-features/src/extension.browser.ts index d582a33606b..456a3811e45 100644 --- a/extensions/markdown-language-features/src/extension.browser.ts +++ b/extensions/markdown-language-features/src/extension.browser.ts @@ -26,6 +26,9 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(workspace); const client = await startServer(context, workspace, engine); + context.subscriptions.push({ + dispose: () => client.stop() + }); activateShared(context, client, workspace, engine, logger, contributions); } diff --git a/extensions/markdown-language-features/src/extension.shared.ts b/extensions/markdown-language-features/src/extension.shared.ts index e3a2e2bd253..4a8e043b520 100644 --- a/extensions/markdown-language-features/src/extension.shared.ts +++ b/extensions/markdown-language-features/src/extension.shared.ts @@ -9,12 +9,10 @@ import { CommandManager } from './commandManager'; import * as commands from './commands/index'; import { registerPasteSupport } from './languageFeatures/copyPaste'; import { registerDiagnosticSupport } from './languageFeatures/diagnostics'; -import { MdLinkProvider } from './languageFeatures/documentLinks'; import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor'; import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences'; -import { MdReferencesProvider } from './languageFeatures/references'; import { ILogger } from './logging'; -import { IMdParser, MarkdownItEngine, MdParsingProvider } from './markdownEngine'; +import { MarkdownItEngine, MdParsingProvider } from './markdownEngine'; import { MarkdownContributionProvider } from './markdownExtensions'; import { MdDocumentRenderer } from './preview/documentRenderer'; import { MarkdownPreviewManager } from './preview/previewManager'; @@ -45,7 +43,7 @@ export function activateShared( const previewManager = new MarkdownPreviewManager(contentProvider, workspace, logger, contributions, tocProvider); context.subscriptions.push(previewManager); - context.subscriptions.push(registerMarkdownLanguageFeatures(client, parser, workspace, commandManager, tocProvider, logger)); + context.subscriptions.push(registerMarkdownLanguageFeatures(client, commandManager)); context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine, tocProvider)); context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => { @@ -55,23 +53,12 @@ export function activateShared( function registerMarkdownLanguageFeatures( client: BaseLanguageClient, - parser: IMdParser, - workspace: IMdWorkspace, commandManager: CommandManager, - tocProvider: MdTableOfContentsProvider, - logger: ILogger, ): vscode.Disposable { const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' }; - - const linkProvider = new MdLinkProvider(parser, workspace, logger); - const referencesProvider = new MdReferencesProvider(parser, workspace, tocProvider, logger); - return vscode.Disposable.from( - linkProvider, - referencesProvider, - // Language features - registerDiagnosticSupport(selector, workspace, linkProvider, commandManager, referencesProvider, tocProvider, logger), + registerDiagnosticSupport(selector, commandManager), registerDropIntoEditorSupport(selector), registerFindFileReferenceSupport(commandManager, client), registerPasteSupport(selector), diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index ff591b3bd2f..9f68ef2c1f2 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -26,6 +26,9 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(workspace); const client = await startServer(context, workspace, engine); + context.subscriptions.push({ + dispose: () => client.stop() + }); activateShared(context, client, workspace, engine, logger, contributions); } diff --git a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts index f403b8ec7d6..6ae36b84aaf 100644 --- a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts @@ -3,609 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as picomatch from 'picomatch'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { CommandManager } from '../commandManager'; -import { ILogger } from '../logging'; -import { MdTableOfContentsProvider } from '../tableOfContents'; -import { ITextDocument } from '../types/textDocument'; -import { Delayer } from '../util/async'; -import { noopToken } from '../util/cancellation'; -import { Disposable } from '../util/dispose'; -import { isMarkdownFile, looksLikeMarkdownPath } from '../util/file'; -import { Limiter } from '../util/limiter'; -import { ResourceMap } from '../util/resourceMap'; -import { MdTableOfContentsWatcher } from '../util/tableOfContentsWatcher'; -import { IMdWorkspace } from '../workspace'; -import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider, MdLinkSource } from './documentLinks'; -import { MdReferencesProvider, tryResolveLinkPath } from './references'; const localize = nls.loadMessageBundle(); -export interface DiagnosticConfiguration { - /** - * Fired when the configuration changes. - */ - readonly onDidChange: vscode.Event<void>; - - getOptions(resource: vscode.Uri): DiagnosticOptions; -} - -export enum DiagnosticLevel { - ignore = 'ignore', - warning = 'warning', - error = 'error', -} - -export interface DiagnosticOptions { - readonly enabled: boolean; - readonly validateReferences: DiagnosticLevel | undefined; - readonly validateFragmentLinks: DiagnosticLevel | undefined; - readonly validateFileLinks: DiagnosticLevel | undefined; - readonly validateMarkdownFileLinkFragments: DiagnosticLevel | undefined; - readonly ignoreLinks: readonly string[]; -} - -function toSeverity(level: DiagnosticLevel | undefined): vscode.DiagnosticSeverity | undefined { - switch (level) { - case DiagnosticLevel.error: return vscode.DiagnosticSeverity.Error; - case DiagnosticLevel.warning: return vscode.DiagnosticSeverity.Warning; - case DiagnosticLevel.ignore: return undefined; - case undefined: return undefined; - } -} - -class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConfiguration { - - private readonly _onDidChange = this._register(new vscode.EventEmitter<void>()); - public readonly onDidChange = this._onDidChange.event; - - constructor() { - super(); - - this._register(vscode.workspace.onDidChangeConfiguration(e => { - if ( - e.affectsConfiguration('markdown.experimental.validate.enabled') - || e.affectsConfiguration('markdown.experimental.validate.referenceLinks.enabled') - || e.affectsConfiguration('markdown.experimental.validate.fragmentLinks.enabled') - || e.affectsConfiguration('markdown.experimental.validate.fileLinks.enabled') - || e.affectsConfiguration('markdown.experimental.validate.fileLinks.markdownFragmentLinks') - || e.affectsConfiguration('markdown.experimental.validate.ignoreLinks') - ) { - this._onDidChange.fire(); - } - })); - } - - public getOptions(resource: vscode.Uri): DiagnosticOptions { - const config = vscode.workspace.getConfiguration('markdown', resource); - const validateFragmentLinks = config.get<DiagnosticLevel>('experimental.validate.fragmentLinks.enabled'); - return { - enabled: config.get<boolean>('experimental.validate.enabled', false), - validateReferences: config.get<DiagnosticLevel>('experimental.validate.referenceLinks.enabled'), - validateFragmentLinks, - validateFileLinks: config.get<DiagnosticLevel>('experimental.validate.fileLinks.enabled'), - validateMarkdownFileLinkFragments: config.get<DiagnosticLevel | undefined>('markdown.experimental.validate.fileLinks.markdownFragmentLinks', validateFragmentLinks), - ignoreLinks: config.get('experimental.validate.ignoreLinks', []), - }; - } -} - -class InflightDiagnosticRequests { - - private readonly inFlightRequests = new ResourceMap<{ readonly cts: vscode.CancellationTokenSource }>(); - - public async trigger(resource: vscode.Uri, compute: (token: vscode.CancellationToken) => Promise<void>): Promise<void> { - this.cancel(resource); - - const cts = new vscode.CancellationTokenSource(); - const entry = { cts }; - this.inFlightRequests.set(resource, entry); - - try { - return await compute(cts.token); - } finally { - if (this.inFlightRequests.get(resource) === entry) { - this.inFlightRequests.delete(resource); - } - cts.dispose(); - } - } - - public cancel(resource: vscode.Uri) { - const existing = this.inFlightRequests.get(resource); - if (existing) { - existing.cts.cancel(); - this.inFlightRequests.delete(resource); - } - } - - public dispose() { - this.clear(); - } - - public clear() { - for (const { cts } of this.inFlightRequests.values()) { - cts.dispose(); - } - this.inFlightRequests.clear(); - } -} - -class LinkWatcher extends Disposable { - - private readonly _onDidChangeLinkedToFile = this._register(new vscode.EventEmitter<Iterable<vscode.Uri>>); - /** - * Event fired with a list of document uri when one of the links in the document changes - */ - public readonly onDidChangeLinkedToFile = this._onDidChangeLinkedToFile.event; - - private readonly _watchers = new ResourceMap<{ - /** - * Watcher for this link path - */ - readonly watcher: vscode.Disposable; - - /** - * List of documents that reference the link - */ - readonly documents: ResourceMap</* document resource*/ vscode.Uri>; - }>(); - - override dispose() { - super.dispose(); - - for (const entry of this._watchers.values()) { - entry.watcher.dispose(); - } - this._watchers.clear(); - } - - /** - * Set the known links in a markdown document, adding and removing file watchers as needed - */ - updateLinksForDocument(document: vscode.Uri, links: readonly MdLink[]) { - const linkedToResource = new Set<vscode.Uri>( - links - .filter(link => link.href.kind === 'internal') - .map(link => (link.href as InternalHref).path)); - - // First decrement watcher counter for previous document state - for (const entry of this._watchers.values()) { - entry.documents.delete(document); - } - - // Then create/update watchers for new document state - for (const path of linkedToResource) { - let entry = this._watchers.get(path); - if (!entry) { - entry = { - watcher: this.startWatching(path), - documents: new ResourceMap(), - }; - this._watchers.set(path, entry); - } - - entry.documents.set(document, document); - } - - // Finally clean up watchers for links that are no longer are referenced anywhere - for (const [key, value] of this._watchers) { - if (value.documents.size === 0) { - value.watcher.dispose(); - this._watchers.delete(key); - } - } - } - - deleteDocument(resource: vscode.Uri) { - this.updateLinksForDocument(resource, []); - } - - private startWatching(path: vscode.Uri): vscode.Disposable { - const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(path, '*'), false, true, false); - const handler = (resource: vscode.Uri) => this.onLinkedResourceChanged(resource); - return vscode.Disposable.from( - watcher, - watcher.onDidDelete(handler), - watcher.onDidCreate(handler), - ); - } - - private onLinkedResourceChanged(resource: vscode.Uri) { - const entry = this._watchers.get(resource); - if (entry) { - this._onDidChangeLinkedToFile.fire(entry.documents.values()); - } - } -} - -class LinkDoesNotExistDiagnostic extends vscode.Diagnostic { - - public readonly link: string; - - constructor(range: vscode.Range, message: string, severity: vscode.DiagnosticSeverity, link: string) { - super(range, message, severity); - this.link = link; - } -} - -export abstract class DiagnosticReporter extends Disposable { - private readonly pending = new Set<Promise<any>>(); - - public clear(): void { - this.pending.clear(); - } - - public abstract set(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]): void; - - public abstract delete(uri: vscode.Uri): void; - - public abstract isOpen(uri: vscode.Uri): boolean; - - public abstract getOpenDocuments(): ITextDocument[]; - - public addWorkItem(promise: Promise<any>): Promise<any> { - this.pending.add(promise); - promise.finally(() => this.pending.delete(promise)); - return promise; - } - - public async waitPendingWork(): Promise<void> { - await Promise.all([...this.pending.values()]); - } -} - -export class DiagnosticCollectionReporter extends DiagnosticReporter { - - private readonly collection: vscode.DiagnosticCollection; - - constructor() { - super(); - this.collection = this._register(vscode.languages.createDiagnosticCollection('markdown')); - } - - public override clear(): void { - super.clear(); - this.collection.clear(); - } - - public set(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]): void { - this.collection.set(uri, this.isOpen(uri) ? diagnostics : []); - } - - public isOpen(uri: vscode.Uri): boolean { - const tabs = this.getTabResources(); - return tabs.has(uri); - } - - public delete(uri: vscode.Uri): void { - this.collection.delete(uri); - } - - public getOpenDocuments(): ITextDocument[] { - const tabs = this.getTabResources(); - return vscode.workspace.textDocuments.filter(doc => tabs.has(doc.uri)); - } - - private getTabResources(): ResourceMap<void> { - const openedTabDocs = new ResourceMap<void>(); - for (const group of vscode.window.tabGroups.all) { - for (const tab of group.tabs) { - if (tab.input instanceof vscode.TabInputText) { - openedTabDocs.set(tab.input.uri); - } - } - } - return openedTabDocs; - } -} - -export class DiagnosticManager extends Disposable { - - private readonly diagnosticDelayer: Delayer<void>; - private readonly pendingDiagnostics = new Set<vscode.Uri>(); - private readonly inFlightDiagnostics = this._register(new InflightDiagnosticRequests()); - - private readonly linkWatcher = this._register(new LinkWatcher()); - private readonly tableOfContentsWatcher: MdTableOfContentsWatcher; - - public readonly ready: Promise<void>; - - constructor( - private readonly workspace: IMdWorkspace, - private readonly computer: DiagnosticComputer, - private readonly configuration: DiagnosticConfiguration, - private readonly reporter: DiagnosticReporter, - private readonly referencesProvider: MdReferencesProvider, - tocProvider: MdTableOfContentsProvider, - private readonly logger: ILogger, - delay = 300, - ) { - super(); - - this.diagnosticDelayer = this._register(new Delayer(delay)); - - this._register(this.configuration.onDidChange(() => { - this.rebuild(); - })); - - this._register(workspace.onDidCreateMarkdownDocument(doc => { - this.triggerDiagnostics(doc.uri); - // Links in other files may have become valid - this.triggerForReferencingFiles(doc.uri); - })); - - this._register(workspace.onDidChangeMarkdownDocument(doc => { - this.triggerDiagnostics(doc.uri); - })); - - this._register(workspace.onDidDeleteMarkdownDocument(uri => { - this.triggerForReferencingFiles(uri); - })); - - this._register(vscode.workspace.onDidCloseTextDocument(({ uri }) => { - this.pendingDiagnostics.delete(uri); - this.inFlightDiagnostics.cancel(uri); - this.linkWatcher.deleteDocument(uri); - this.reporter.delete(uri); - })); - - this._register(this.linkWatcher.onDidChangeLinkedToFile(changedDocuments => { - for (const resource of changedDocuments) { - const doc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === resource.toString()); - if (doc && isMarkdownFile(doc)) { - this.triggerDiagnostics(doc.uri); - } - } - })); - - this.tableOfContentsWatcher = this._register(new MdTableOfContentsWatcher(workspace, tocProvider, delay / 2)); - this._register(this.tableOfContentsWatcher.onTocChanged(e => { - return this.triggerForReferencingFiles(e.uri); - })); - - this.ready = this.rebuild(); - } - - private triggerForReferencingFiles(uri: vscode.Uri): Promise<void> { - return this.reporter.addWorkItem( - (async () => { - const triggered = new ResourceMap<Promise<void>>(); - for (const ref of await this.referencesProvider.getReferencesToFileInDocs(uri, this.reporter.getOpenDocuments(), noopToken)) { - const file = ref.location.uri; - if (!triggered.has(file)) { - triggered.set(file, this.triggerDiagnostics(file)); - } - } - await Promise.all(triggered.values()); - })()); - } - - public override dispose() { - super.dispose(); - this.pendingDiagnostics.clear(); - } - - private async recomputeDiagnosticState(doc: ITextDocument, token: vscode.CancellationToken): Promise<{ diagnostics: readonly vscode.Diagnostic[]; links: readonly MdLink[]; config: DiagnosticOptions }> { - this.logger.verbose('DiagnosticManager', `recomputeDiagnosticState - ${doc.uri}`); - - const config = this.configuration.getOptions(doc.uri); - if (!config.enabled) { - return { diagnostics: [], links: [], config }; - } - return { ...await this.computer.getDiagnostics(doc, config, token), config }; - } - - private async recomputePendingDiagnostics(): Promise<void> { - const pending = [...this.pendingDiagnostics]; - this.pendingDiagnostics.clear(); - - await Promise.all(pending.map(async resource => { - const doc = await this.workspace.getOrLoadMarkdownDocument(resource); - if (doc) { - await this.inFlightDiagnostics.trigger(doc.uri, async (token) => { - if (this.reporter.isOpen(doc.uri)) { - const state = await this.recomputeDiagnosticState(doc, token); - this.linkWatcher.updateLinksForDocument(doc.uri, state.config.enabled && state.config.validateFileLinks ? state.links : []); - this.reporter.set(doc.uri, state.diagnostics); - } else { - this.linkWatcher.deleteDocument(doc.uri); - this.reporter.delete(doc.uri); - } - }); - } - })); - } - - private rebuild(): Promise<void> { - this.reporter.clear(); - this.pendingDiagnostics.clear(); - this.inFlightDiagnostics.clear(); - - return this.reporter.addWorkItem( - Promise.all(Array.from(this.reporter.getOpenDocuments(), doc => this.triggerDiagnostics(doc.uri))) - ); - } - - private async triggerDiagnostics(uri: vscode.Uri): Promise<void> { - this.inFlightDiagnostics.cancel(uri); - - this.pendingDiagnostics.add(uri); - return this.reporter.addWorkItem( - this.diagnosticDelayer.trigger(() => this.recomputePendingDiagnostics()) - ); - } +// Copied from markdown language service +export enum DiagnosticCode { + link_noSuchReferences = 'link.no-such-reference', + link_noSuchHeaderInOwnFile = 'link.no-such-header-in-own-file', + link_noSuchFile = 'link.no-such-file', + link_noSuchHeaderInFile = 'link.no-such-header-in-file', } -/** - * Map of file paths to markdown links to that file. - */ -class FileLinkMap { - - private readonly _filesToLinksMap = new ResourceMap<{ - readonly outgoingLinks: Array<{ - readonly source: MdLinkSource; - readonly fragment: string; - }>; - }>(); - - constructor(links: Iterable<MdLink>) { - for (const link of links) { - if (link.href.kind !== 'internal') { - continue; - } - - const existingFileEntry = this._filesToLinksMap.get(link.href.path); - const linkData = { source: link.source, fragment: link.href.fragment }; - if (existingFileEntry) { - existingFileEntry.outgoingLinks.push(linkData); - } else { - this._filesToLinksMap.set(link.href.path, { outgoingLinks: [linkData] }); - } - } - } - - public get size(): number { - return this._filesToLinksMap.size; - } - - public entries() { - return this._filesToLinksMap.entries(); - } -} - -export class DiagnosticComputer { - - constructor( - private readonly workspace: IMdWorkspace, - private readonly linkProvider: MdLinkProvider, - private readonly tocProvider: MdTableOfContentsProvider, - ) { } - - public async getDiagnostics(doc: ITextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise<{ readonly diagnostics: vscode.Diagnostic[]; readonly links: readonly MdLink[] }> { - const { links, definitions } = await this.linkProvider.getLinks(doc); - if (token.isCancellationRequested || !options.enabled) { - return { links, diagnostics: [] }; - } - - return { - links, - diagnostics: (await Promise.all([ - this.validateFileLinks(options, links, token), - Array.from(this.validateReferenceLinks(options, links, definitions)), - this.validateFragmentLinks(doc, options, links, token), - ])).flat() - }; - } - - private async validateFragmentLinks(doc: ITextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> { - const severity = toSeverity(options.validateFragmentLinks); - if (typeof severity === 'undefined') { - return []; - } - - const toc = await this.tocProvider.getForDocument(doc); - if (token.isCancellationRequested) { - return []; - } - - const diagnostics: vscode.Diagnostic[] = []; - for (const link of links) { - if (link.href.kind === 'internal' - && link.source.hrefText.startsWith('#') - && link.href.path.toString() === doc.uri.toString() - && link.href.fragment - && !toc.lookup(link.href.fragment) - ) { - if (!this.isIgnoredLink(options, link.source.hrefText)) { - diagnostics.push(new LinkDoesNotExistDiagnostic( - link.source.hrefRange, - localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment), - severity, - link.source.hrefText)); - } - } - } - - return diagnostics; - } - - private *validateReferenceLinks(options: DiagnosticOptions, links: readonly MdLink[], definitions: LinkDefinitionSet): Iterable<vscode.Diagnostic> { - const severity = toSeverity(options.validateReferences); - if (typeof severity === 'undefined') { - return []; - } - - for (const link of links) { - if (link.href.kind === 'reference' && !definitions.lookup(link.href.ref)) { - yield new vscode.Diagnostic( - link.source.hrefRange, - localize('invalidReferenceLink', 'No link definition found: \'{0}\'', link.href.ref), - severity); - } - } - } - - private async validateFileLinks(options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> { - const pathErrorSeverity = toSeverity(options.validateFileLinks); - if (typeof pathErrorSeverity === 'undefined') { - return []; - } - const fragmentErrorSeverity = toSeverity(typeof options.validateMarkdownFileLinkFragments === 'undefined' ? options.validateFragmentLinks : options.validateMarkdownFileLinkFragments); - - // We've already validated our own fragment links in `validateOwnHeaderLinks` - const linkSet = new FileLinkMap(links.filter(link => !link.source.hrefText.startsWith('#'))); - if (linkSet.size === 0) { - return []; - } - - const limiter = new Limiter(10); - - const diagnostics: vscode.Diagnostic[] = []; - await Promise.all( - Array.from(linkSet.entries()).map(([path, { outgoingLinks: links }]) => { - return limiter.queue(async () => { - if (token.isCancellationRequested) { - return; - } - - const resolvedHrefPath = await tryResolveLinkPath(path, this.workspace); - if (!resolvedHrefPath) { - const msg = localize('invalidPathLink', 'File does not exist at path: {0}', path.fsPath); - for (const link of links) { - if (!this.isIgnoredLink(options, link.source.pathText)) { - diagnostics.push(new LinkDoesNotExistDiagnostic(link.source.hrefRange, msg, pathErrorSeverity, link.source.pathText)); - } - } - } else if (typeof fragmentErrorSeverity !== 'undefined' && this.isMarkdownPath(resolvedHrefPath)) { - // Validate each of the links to headers in the file - const fragmentLinks = links.filter(x => x.fragment); - if (fragmentLinks.length) { - const toc = await this.tocProvider.get(resolvedHrefPath); - for (const link of fragmentLinks) { - if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.pathText) && !this.isIgnoredLink(options, link.source.hrefText)) { - const msg = localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', link.fragment); - const range = link.source.fragmentRange?.with({ start: link.source.fragmentRange.start.translate(0, -1) }) ?? link.source.hrefRange; - diagnostics.push(new LinkDoesNotExistDiagnostic(range, msg, fragmentErrorSeverity, link.source.hrefText)); - } - } - } - } - }); - })); - return diagnostics; - } - - private isMarkdownPath(resolvedHrefPath: vscode.Uri) { - return this.workspace.hasMarkdownDocument(resolvedHrefPath) || looksLikeMarkdownPath(resolvedHrefPath); - } - - private isIgnoredLink(options: DiagnosticOptions, link: string): boolean { - return options.ignoreLinks.some(glob => picomatch.isMatch(link, glob)); - } -} class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider { @@ -636,17 +47,26 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider { const fixes: vscode.CodeAction[] = []; for (const diagnostic of context.diagnostics) { - if (diagnostic instanceof LinkDoesNotExistDiagnostic) { - const fix = new vscode.CodeAction( - localize('ignoreLinksQuickFix.title', "Exclude '{0}' from link validation.", diagnostic.link), - vscode.CodeActionKind.QuickFix); - - fix.command = { - command: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId, - title: '', - arguments: [document.uri, diagnostic.link] - }; - fixes.push(fix); + switch (diagnostic.code) { + case DiagnosticCode.link_noSuchReferences: + case DiagnosticCode.link_noSuchHeaderInOwnFile: + case DiagnosticCode.link_noSuchFile: + case DiagnosticCode.link_noSuchHeaderInFile: { + const hrefText = (diagnostic as any).data?.hrefText; + if (hrefText) { + const fix = new vscode.CodeAction( + localize('ignoreLinksQuickFix.title', "Exclude '{0}' from link validation.", hrefText), + vscode.CodeActionKind.QuickFix); + + fix.command = { + command: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId, + title: '', + arguments: [document.uri, hrefText], + }; + fixes.push(fix); + } + break; + } } } @@ -654,26 +74,10 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider { } } + export function registerDiagnosticSupport( selector: vscode.DocumentSelector, - workspace: IMdWorkspace, - linkProvider: MdLinkProvider, commandManager: CommandManager, - referenceProvider: MdReferencesProvider, - tocProvider: MdTableOfContentsProvider, - logger: ILogger, ): vscode.Disposable { - const configuration = new VSCodeDiagnosticConfiguration(); - const manager = new DiagnosticManager( - workspace, - new DiagnosticComputer(workspace, linkProvider, tocProvider), - configuration, - new DiagnosticCollectionReporter(), - referenceProvider, - tocProvider, - logger); - return vscode.Disposable.from( - configuration, - manager, - AddToIgnoreLinksQuickFixProvider.register(selector, commandManager)); + return AddToIgnoreLinksQuickFixProvider.register(selector, commandManager); } diff --git a/extensions/markdown-language-features/src/languageFeatures/documentLinks.ts b/extensions/markdown-language-features/src/languageFeatures/documentLinks.ts deleted file mode 100644 index 449be42595b..00000000000 --- a/extensions/markdown-language-features/src/languageFeatures/documentLinks.ts +++ /dev/null @@ -1,540 +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 * as uri from 'vscode-uri'; -import { ILogger } from '../logging'; -import { IMdParser } from '../markdownEngine'; -import { getLine, ITextDocument } from '../types/textDocument'; -import { noopToken } from '../util/cancellation'; -import { Disposable } from '../util/dispose'; -import { Schemes } from '../util/schemes'; -import { MdDocumentInfoCache } from '../util/workspaceCache'; -import { IMdWorkspace } from '../workspace'; - -export interface ExternalHref { - readonly kind: 'external'; - readonly uri: vscode.Uri; -} - -export interface InternalHref { - readonly kind: 'internal'; - readonly path: vscode.Uri; - readonly fragment: string; -} - -export interface ReferenceHref { - readonly kind: 'reference'; - readonly ref: string; -} - -export type LinkHref = ExternalHref | InternalHref | ReferenceHref; - - -function resolveLink( - document: ITextDocument, - link: string, -): ExternalHref | InternalHref | undefined { - const cleanLink = stripAngleBrackets(link); - - if (/^[a-z\-][a-z\-]+:/i.test(cleanLink)) { - // Looks like a uri - return { kind: 'external', uri: vscode.Uri.parse(cleanLink) }; - } - - // Assume it must be an relative or absolute file path - // Use a fake scheme to avoid parse warnings - const tempUri = vscode.Uri.parse(`vscode-resource:${link}`); - - let resourceUri: vscode.Uri | undefined; - if (!tempUri.path) { - resourceUri = document.uri; - } else if (tempUri.path[0] === '/') { - const root = getWorkspaceFolder(document); - if (root) { - resourceUri = vscode.Uri.joinPath(root, tempUri.path); - } - } else { - if (document.uri.scheme === Schemes.untitled) { - const root = getWorkspaceFolder(document); - if (root) { - resourceUri = vscode.Uri.joinPath(root, tempUri.path); - } - } else { - const base = uri.Utils.dirname(document.uri); - resourceUri = vscode.Uri.joinPath(base, tempUri.path); - } - } - - if (!resourceUri) { - return undefined; - } - - // If we are in a notebook cell, resolve relative to notebook instead - if (resourceUri.scheme === Schemes.notebookCell) { - const notebook = vscode.workspace.notebookDocuments - .find(notebook => notebook.getCells().some(cell => cell.document === document)); - - if (notebook) { - resourceUri = resourceUri.with({ scheme: notebook.uri.scheme }); - } - } - - return { - kind: 'internal', - path: resourceUri.with({ fragment: '' }), - fragment: tempUri.fragment, - }; -} - -function getWorkspaceFolder(document: ITextDocument) { - return vscode.workspace.getWorkspaceFolder(document.uri)?.uri - || vscode.workspace.workspaceFolders?.[0]?.uri; -} - -export interface MdLinkSource { - /** - * The full range of the link. - */ - readonly range: vscode.Range; - - /** - * The file where the link is defined. - */ - readonly resource: vscode.Uri; - - /** - * The original text of the link destination in code. - */ - readonly hrefText: string; - - /** - * The original text of just the link's path in code. - */ - readonly pathText: string; - - /** - * The range of the path. - */ - readonly hrefRange: vscode.Range; - - /** - * The range of the fragment within the path. - */ - readonly fragmentRange: vscode.Range | undefined; -} - -export interface MdInlineLink { - readonly kind: 'link'; - readonly source: MdLinkSource; - readonly href: LinkHref; -} - -export interface MdLinkDefinition { - readonly kind: 'definition'; - readonly source: MdLinkSource; - readonly ref: { - readonly range: vscode.Range; - readonly text: string; - }; - readonly href: ExternalHref | InternalHref; -} - -export type MdLink = MdInlineLink | MdLinkDefinition; - -function extractDocumentLink( - document: ITextDocument, - pre: string, - rawLink: string, - matchIndex: number, - fullMatch: string, -): MdLink | undefined { - const isAngleBracketLink = rawLink.startsWith('<'); - const link = stripAngleBrackets(rawLink); - - let linkTarget: ExternalHref | InternalHref | undefined; - try { - linkTarget = resolveLink(document, link); - } catch { - return undefined; - } - if (!linkTarget) { - return undefined; - } - - const linkStart = document.positionAt(matchIndex); - const linkEnd = linkStart.translate(0, fullMatch.length); - const hrefStart = linkStart.translate(0, pre.length + (isAngleBracketLink ? 1 : 0)); - const hrefEnd = hrefStart.translate(0, link.length); - return { - kind: 'link', - href: linkTarget, - source: { - hrefText: link, - resource: document.uri, - range: new vscode.Range(linkStart, linkEnd), - hrefRange: new vscode.Range(hrefStart, hrefEnd), - ...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd), - } - }; -} - -function getFragmentRange(text: string, start: vscode.Position, end: vscode.Position): vscode.Range | undefined { - const index = text.indexOf('#'); - if (index < 0) { - return undefined; - } - return new vscode.Range(start.translate({ characterDelta: index + 1 }), end); -} - -function getLinkSourceFragmentInfo(document: ITextDocument, link: string, linkStart: vscode.Position, linkEnd: vscode.Position): { fragmentRange: vscode.Range | undefined; pathText: string } { - const fragmentRange = getFragmentRange(link, linkStart, linkEnd); - return { - pathText: document.getText(new vscode.Range(linkStart, fragmentRange ? fragmentRange.start.translate(0, -1) : linkEnd)), - fragmentRange, - }; -} - -const angleBracketLinkRe = /^<(.*)>$/; - -/** - * Used to strip brackets from the markdown link - * - * <http://example.com> will be transformed to http://example.com -*/ -function stripAngleBrackets(link: string) { - return link.replace(angleBracketLinkRe, '$1'); -} - -const r = String.raw; - -/** - * Matches `[text](link)` or `[text](<link>)` - */ -const linkPattern = new RegExp( - // text - r`(\[` + // open prefix match --> - /**/r`(?:` + - /*****/r`[^\[\]\\]|` + // Non-bracket chars, or... - /*****/r`\\.|` + // Escaped char, or... - /*****/r`\[[^\[\]]*\]` + // Matched bracket pair - /**/r`)*` + - r`\]` + - - // Destination - r`\(\s*)` + // <-- close prefix match - /**/r`(` + - /*****/r`[^\s\(\)\<](?:[^\s\(\)]|\([^\s\(\)]*?\))*|` + // Link without whitespace, or... - /*****/r`<[^<>]+>` + // In angle brackets - /**/r`)` + - - // Title - /**/r`\s*(?:"[^"]*"|'[^']*'|\([^\(\)]*\))?\s*` + - r`\)`, - 'g'); - -/** -* Matches `[text][ref]` or `[shorthand]` -*/ -const referenceLinkPattern = /(^|[^\]\\])(?:(?:(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]|\[\s*?([^\s\\\]]*?)\])(?![\:\(]))/gm; - -/** - * Matches `<http://example.com>` - */ -const autoLinkPattern = /\<(\w+:[^\>\s]+)\>/g; - -/** - * Matches `[text]: link` - */ -const definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)([^<]\S*|<[^>]+>)/gm; - -const inlineCodePattern = /(?:^|[^`])(`+)(?:.+?|.*?(?:(?:\r?\n).+?)*?)(?:\r?\n)?\1(?:$|[^`])/gm; - -class NoLinkRanges { - public static async compute(tokenizer: IMdParser, document: ITextDocument): Promise<NoLinkRanges> { - const tokens = await tokenizer.tokenize(document); - const multiline = tokens.filter(t => (t.type === 'code_block' || t.type === 'fence' || t.type === 'html_block') && !!t.map).map(t => t.map) as [number, number][]; - - const inlineRanges = new Map</* line number */ number, vscode.Range[]>(); - const text = document.getText(); - for (const match of text.matchAll(inlineCodePattern)) { - const startOffset = match.index ?? 0; - const startPosition = document.positionAt(startOffset); - - const range = new vscode.Range(startPosition, document.positionAt(startOffset + match[0].length)); - for (let line = range.start.line; line <= range.end.line; ++line) { - let entry = inlineRanges.get(line); - if (!entry) { - entry = []; - inlineRanges.set(line, entry); - } - entry.push(range); - } - } - - return new NoLinkRanges(multiline, inlineRanges); - } - - private constructor( - /** - * code blocks and fences each represented by [line_start,line_end). - */ - public readonly multiline: ReadonlyArray<[number, number]>, - - /** - * Inline code spans where links should not be detected - */ - public readonly inline: Map</* line number */ number, vscode.Range[]> - ) { } - - contains(position: vscode.Position): boolean { - return this.multiline.some(interval => position.line >= interval[0] && position.line < interval[1]) || - !!this.inline.get(position.line)?.some(inlineRange => inlineRange.contains(position)); - } - - concatInline(inlineRanges: Iterable<vscode.Range>): NoLinkRanges { - const newInline = new Map(this.inline); - for (const range of inlineRanges) { - for (let line = range.start.line; line <= range.end.line; ++line) { - let entry = newInline.get(line); - if (!entry) { - entry = []; - newInline.set(line, entry); - } - entry.push(range); - } - } - return new NoLinkRanges(this.multiline, newInline); - } -} - -/** - * Stateless object that extracts link information from markdown files. - */ -export class MdLinkComputer { - - constructor( - private readonly tokenizer: IMdParser, - ) { } - - public async getAllLinks(document: ITextDocument, token: vscode.CancellationToken): Promise<MdLink[]> { - const noLinkRanges = await NoLinkRanges.compute(this.tokenizer, document); - if (token.isCancellationRequested) { - return []; - } - - const inlineLinks = Array.from(this.getInlineLinks(document, noLinkRanges)); - return Array.from([ - ...inlineLinks, - ...this.getReferenceLinks(document, noLinkRanges.concatInline(inlineLinks.map(x => x.source.range))), - ...this.getLinkDefinitions(document, noLinkRanges), - ...this.getAutoLinks(document, noLinkRanges), - ]); - } - - private *getInlineLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> { - const text = document.getText(); - for (const match of text.matchAll(linkPattern)) { - const matchLinkData = extractDocumentLink(document, match[1], match[2], match.index ?? 0, match[0]); - if (matchLinkData && !noLinkRanges.contains(matchLinkData.source.hrefRange.start)) { - yield matchLinkData; - - // Also check link destination for links - for (const innerMatch of match[1].matchAll(linkPattern)) { - const innerData = extractDocumentLink(document, innerMatch[1], innerMatch[2], (match.index ?? 0) + (innerMatch.index ?? 0), innerMatch[0]); - if (innerData) { - yield innerData; - } - } - } - } - } - - private *getAutoLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> { - const text = document.getText(); - for (const match of text.matchAll(autoLinkPattern)) { - const linkOffset = (match.index ?? 0); - const linkStart = document.positionAt(linkOffset); - if (noLinkRanges.contains(linkStart)) { - continue; - } - - const link = match[1]; - const linkTarget = resolveLink(document, link); - if (!linkTarget) { - continue; - } - - const linkEnd = linkStart.translate(0, match[0].length); - const hrefStart = linkStart.translate(0, 1); - const hrefEnd = hrefStart.translate(0, link.length); - yield { - kind: 'link', - href: linkTarget, - source: { - hrefText: link, - resource: document.uri, - hrefRange: new vscode.Range(hrefStart, hrefEnd), - range: new vscode.Range(linkStart, linkEnd), - ...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd), - } - }; - } - } - - private *getReferenceLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> { - const text = document.getText(); - for (const match of text.matchAll(referenceLinkPattern)) { - const linkStartOffset = (match.index ?? 0) + match[1].length; - const linkStart = document.positionAt(linkStartOffset); - if (noLinkRanges.contains(linkStart)) { - continue; - } - - let hrefStart: vscode.Position; - let hrefEnd: vscode.Position; - let reference = match[4]; - if (reference === '') { // [ref][], - reference = match[3]; - const offset = linkStartOffset + 1; - hrefStart = document.positionAt(offset); - hrefEnd = document.positionAt(offset + reference.length); - } else if (reference) { // [text][ref] - const pre = match[2]; - const offset = linkStartOffset + pre.length; - hrefStart = document.positionAt(offset); - hrefEnd = document.positionAt(offset + reference.length); - } else if (match[5]) { // [ref] - reference = match[5]; - const offset = linkStartOffset + 1; - hrefStart = document.positionAt(offset); - const line = getLine(document, hrefStart.line); - // See if link looks like a checkbox - const checkboxMatch = line.match(/^\s*[\-\*]\s*\[x\]/i); - if (checkboxMatch && hrefStart.character <= checkboxMatch[0].length) { - continue; - } - hrefEnd = document.positionAt(offset + reference.length); - } else { - continue; - } - - const linkEnd = linkStart.translate(0, match[0].length - match[1].length); - yield { - kind: 'link', - source: { - hrefText: reference, - pathText: reference, - resource: document.uri, - range: new vscode.Range(linkStart, linkEnd), - hrefRange: new vscode.Range(hrefStart, hrefEnd), - fragmentRange: undefined, - }, - href: { - kind: 'reference', - ref: reference, - } - }; - } - } - - private *getLinkDefinitions(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLinkDefinition> { - const text = document.getText(); - for (const match of text.matchAll(definitionPattern)) { - const offset = (match.index ?? 0); - const linkStart = document.positionAt(offset); - if (noLinkRanges.contains(linkStart)) { - continue; - } - - const pre = match[1]; - const reference = match[2]; - const rawLinkText = match[3].trim(); - const target = resolveLink(document, rawLinkText); - if (!target) { - continue; - } - - const isAngleBracketLink = angleBracketLinkRe.test(rawLinkText); - const linkText = stripAngleBrackets(rawLinkText); - const hrefStart = linkStart.translate(0, pre.length + (isAngleBracketLink ? 1 : 0)); - const hrefEnd = hrefStart.translate(0, linkText.length); - const hrefRange = new vscode.Range(hrefStart, hrefEnd); - - const refStart = linkStart.translate(0, 1); - const refRange = new vscode.Range(refStart, refStart.translate({ characterDelta: reference.length })); - const linkEnd = linkStart.translate(0, match[0].length); - yield { - kind: 'definition', - source: { - hrefText: linkText, - resource: document.uri, - range: new vscode.Range(linkStart, linkEnd), - hrefRange, - ...getLinkSourceFragmentInfo(document, rawLinkText, hrefStart, hrefEnd), - }, - ref: { text: reference, range: refRange }, - href: target, - }; - } - } -} - -interface MdDocumentLinks { - readonly links: readonly MdLink[]; - readonly definitions: LinkDefinitionSet; -} - -/** - * Stateful object which provides links for markdown files the workspace. - */ -export class MdLinkProvider extends Disposable { - - private readonly _linkCache: MdDocumentInfoCache<MdDocumentLinks>; - - private readonly linkComputer: MdLinkComputer; - - constructor( - tokenizer: IMdParser, - workspace: IMdWorkspace, - logger: ILogger, - ) { - super(); - this.linkComputer = new MdLinkComputer(tokenizer); - this._linkCache = this._register(new MdDocumentInfoCache(workspace, async doc => { - logger.verbose('LinkProvider', `compute - ${doc.uri}`); - - const links = await this.linkComputer.getAllLinks(doc, noopToken); - return { - links, - definitions: new LinkDefinitionSet(links), - }; - })); - } - - public async getLinks(document: ITextDocument): Promise<MdDocumentLinks> { - return this._linkCache.getForDocument(document); - } -} - -export class LinkDefinitionSet implements Iterable<[string, MdLinkDefinition]> { - private readonly _map = new Map<string, MdLinkDefinition>(); - - constructor(links: Iterable<MdLink>) { - for (const link of links) { - if (link.kind === 'definition') { - this._map.set(link.ref.text, link); - } - } - } - - public [Symbol.iterator](): Iterator<[string, MdLinkDefinition]> { - return this._map.entries(); - } - - public lookup(ref: string): MdLinkDefinition | undefined { - return this._map.get(ref); - } -} diff --git a/extensions/markdown-language-features/src/languageFeatures/references.ts b/extensions/markdown-language-features/src/languageFeatures/references.ts deleted file mode 100644 index 5aada05300e..00000000000 --- a/extensions/markdown-language-features/src/languageFeatures/references.ts +++ /dev/null @@ -1,329 +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 * as uri from 'vscode-uri'; -import { ILogger } from '../logging'; -import { IMdParser } from '../markdownEngine'; -import { MdTableOfContentsProvider, TocEntry } from '../tableOfContents'; -import { ITextDocument } from '../types/textDocument'; -import { noopToken } from '../util/cancellation'; -import { Disposable } from '../util/dispose'; -import { looksLikeMarkdownPath } from '../util/file'; -import { MdWorkspaceInfoCache } from '../util/workspaceCache'; -import { IMdWorkspace } from '../workspace'; -import { InternalHref, MdLink, MdLinkComputer } from './documentLinks'; - - -/** - * A link in a markdown file. - */ -export interface MdLinkReference { - readonly kind: 'link'; - readonly isTriggerLocation: boolean; - readonly isDefinition: boolean; - readonly location: vscode.Location; - - readonly link: MdLink; -} - -/** - * A header in a markdown file. - */ -export interface MdHeaderReference { - readonly kind: 'header'; - - readonly isTriggerLocation: boolean; - readonly isDefinition: boolean; - - /** - * The range of the header. - * - * In `# a b c #` this would be the range of `# a b c #` - */ - readonly location: vscode.Location; - - /** - * The text of the header. - * - * In `# a b c #` this would be `a b c` - */ - readonly headerText: string; - - /** - * The range of the header text itself. - * - * In `# a b c #` this would be the range of `a b c` - */ - readonly headerTextLocation: vscode.Location; -} - -export type MdReference = MdLinkReference | MdHeaderReference; - -/** - * Stateful object that computes references for markdown files. - */ -export class MdReferencesProvider extends Disposable { - - private readonly _linkCache: MdWorkspaceInfoCache<readonly MdLink[]>; - - public constructor( - private readonly parser: IMdParser, - private readonly workspace: IMdWorkspace, - private readonly tocProvider: MdTableOfContentsProvider, - private readonly logger: ILogger, - ) { - super(); - - const linkComputer = new MdLinkComputer(parser); - this._linkCache = this._register(new MdWorkspaceInfoCache(workspace, doc => linkComputer.getAllLinks(doc, noopToken))); - } - - public async getReferencesAtPosition(document: ITextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> { - this.logger.verbose('ReferencesProvider', `getReferencesAtPosition: ${document.uri}`); - - const toc = await this.tocProvider.getForDocument(document); - if (token.isCancellationRequested) { - return []; - } - - const header = toc.entries.find(entry => entry.line === position.line); - if (header) { - return this.getReferencesToHeader(document, header); - } else { - return this.getReferencesToLinkAtPosition(document, position, token); - } - } - - public async getReferencesToFileInWorkspace(resource: vscode.Uri, token: vscode.CancellationToken): Promise<MdReference[]> { - this.logger.verbose('ReferencesProvider', `getAllReferencesToFileInWorkspace: ${resource}`); - - const allLinksInWorkspace = (await this._linkCache.values()).flat(); - if (token.isCancellationRequested) { - return []; - } - - return Array.from(this.findLinksToFile(resource, allLinksInWorkspace, undefined)); - } - - public async getReferencesToFileInDocs(resource: vscode.Uri, otherDocs: readonly ITextDocument[], token: vscode.CancellationToken): Promise<MdReference[]> { - this.logger.verbose('ReferencesProvider', `getAllReferencesToFileInFiles: ${resource}`); - - const links = (await this._linkCache.getForDocs(otherDocs)).flat(); - if (token.isCancellationRequested) { - return []; - } - - return Array.from(this.findLinksToFile(resource, links, undefined)); - } - - private async getReferencesToHeader(document: ITextDocument, header: TocEntry): Promise<MdReference[]> { - const links = (await this._linkCache.values()).flat(); - - const references: MdReference[] = []; - - references.push({ - kind: 'header', - isTriggerLocation: true, - isDefinition: true, - location: header.headerLocation, - headerText: header.text, - headerTextLocation: header.headerTextLocation - }); - - for (const link of links) { - if (link.href.kind === 'internal' - && this.looksLikeLinkToDoc(link.href, document.uri) - && this.parser.slugifier.fromHeading(link.href.fragment).value === header.slug.value - ) { - references.push({ - kind: 'link', - isTriggerLocation: false, - isDefinition: false, - link, - location: new vscode.Location(link.source.resource, link.source.hrefRange), - }); - } - } - - return references; - } - - private async getReferencesToLinkAtPosition(document: ITextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> { - const docLinks = (await this._linkCache.getForDocs([document]))[0]; - - for (const link of docLinks) { - if (link.kind === 'definition') { - // We could be in either the ref name or the definition - 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, position, token); - } - } else { - if (link.source.hrefRange.contains(position)) { - return this.getReferencesToLink(link, position, token); - } - } - } - - return []; - } - - private async getReferencesToLink(sourceLink: MdLink, triggerPosition: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> { - const allLinksInWorkspace = (await this._linkCache.values()).flat(); - if (token.isCancellationRequested) { - return []; - } - - if (sourceLink.href.kind === 'reference') { - return Array.from(this.getReferencesToLinkReference(allLinksInWorkspace, sourceLink.href.ref, { resource: sourceLink.source.resource, range: sourceLink.source.hrefRange })); - } - - if (sourceLink.href.kind === 'external') { - const references: MdReference[] = []; - - for (const link of allLinksInWorkspace) { - if (link.href.kind === 'external' && link.href.uri.toString() === sourceLink.href.uri.toString()) { - const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange); - references.push({ - kind: 'link', - isTriggerLocation, - isDefinition: false, - link, - location: new vscode.Location(link.source.resource, link.source.hrefRange), - }); - } - } - return references; - } - - const resolvedResource = await tryResolveLinkPath(sourceLink.href.path, this.workspace); - if (token.isCancellationRequested) { - return []; - } - - const references: MdReference[] = []; - - if (resolvedResource && this.isMarkdownPath(resolvedResource) && sourceLink.href.fragment && sourceLink.source.fragmentRange?.contains(triggerPosition)) { - const toc = await this.tocProvider.get(resolvedResource); - const entry = toc.lookup(sourceLink.href.fragment); - if (entry) { - references.push({ - kind: 'header', - isTriggerLocation: false, - isDefinition: true, - location: entry.headerLocation, - headerText: entry.text, - headerTextLocation: entry.headerTextLocation - }); - } - - for (const link of allLinksInWorkspace) { - if (link.href.kind !== 'internal' || !this.looksLikeLinkToDoc(link.href, resolvedResource)) { - continue; - } - - if (this.parser.slugifier.fromHeading(link.href.fragment).equals(this.parser.slugifier.fromHeading(sourceLink.href.fragment))) { - const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange); - references.push({ - kind: 'link', - isTriggerLocation, - isDefinition: false, - link, - location: new vscode.Location(link.source.resource, link.source.hrefRange), - }); - } - } - } else { // Triggered on a link without a fragment so we only require matching the file and ignore fragments - references.push(...this.findLinksToFile(resolvedResource ?? sourceLink.href.path, allLinksInWorkspace, sourceLink)); - } - - return references; - } - - private isMarkdownPath(resolvedHrefPath: vscode.Uri) { - return this.workspace.hasMarkdownDocument(resolvedHrefPath) || looksLikeMarkdownPath(resolvedHrefPath); - } - - private looksLikeLinkToDoc(href: InternalHref, targetDoc: vscode.Uri) { - return href.path.fsPath === targetDoc.fsPath - || uri.Utils.extname(href.path) === '' && href.path.with({ path: href.path.path + '.md' }).fsPath === targetDoc.fsPath; - } - - private *findLinksToFile(resource: vscode.Uri, links: readonly MdLink[], sourceLink: MdLink | undefined): Iterable<MdReference> { - for (const link of links) { - if (link.href.kind !== 'internal' || !this.looksLikeLinkToDoc(link.href, resource)) { - continue; - } - - // Exclude cases where the file is implicitly referencing itself - if (link.source.hrefText.startsWith('#') && link.source.resource.fsPath === resource.fsPath) { - continue; - } - - 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, pathRange), - }; - } - } - - private *getReferencesToLinkReference(allLinks: Iterable<MdLink>, refToFind: string, from: { resource: vscode.Uri; range: vscode.Range }): Iterable<MdReference> { - for (const link of allLinks) { - let ref: string; - if (link.kind === 'definition') { - ref = link.ref.text; - } else if (link.href.kind === 'reference') { - ref = link.href.ref; - } else { - continue; - } - - 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, 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 tryResolveLinkPath(originalUri: vscode.Uri, workspace: IMdWorkspace): Promise<vscode.Uri | undefined> { - if (await workspace.pathExists(originalUri)) { - return originalUri; - } - - // 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(originalUri) === '') { - const dotMdResource = originalUri.with({ path: originalUri.path + '.md' }); - if (await workspace.pathExists(dotMdResource)) { - return dotMdResource; - } - } - - return undefined; -} diff --git a/extensions/markdown-language-features/src/logging.ts b/extensions/markdown-language-features/src/logging.ts index 48a2f3a7709..5947af62409 100644 --- a/extensions/markdown-language-features/src/logging.ts +++ b/extensions/markdown-language-features/src/logging.ts @@ -70,7 +70,7 @@ export class VsCodeOutputLogger extends Disposable implements ILogger { } private readTrace(): Trace { - return Trace.fromString(vscode.workspace.getConfiguration().get<string>('markdown.trace', 'off')); + return Trace.fromString(vscode.workspace.getConfiguration().get<string>('markdown.trace.extension', 'off')); } private static data2String(data: any): string { diff --git a/extensions/markdown-language-features/src/protocol.ts b/extensions/markdown-language-features/src/protocol.ts index 75b8162cf8c..53bb27b9822 100644 --- a/extensions/markdown-language-features/src/protocol.ts +++ b/extensions/markdown-language-features/src/protocol.ts @@ -6,13 +6,23 @@ import Token = require('markdown-it/lib/token'); import { RequestType } from 'vscode-languageclient'; import type * as lsp from 'vscode-languageserver-types'; +import type * as md from 'vscode-markdown-languageservice'; -// From server -export const parseRequestType: RequestType<{ uri: string }, Token[], any> = new RequestType('markdown/parse'); -export const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile'); -export const statFileRequestType: RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any> = new RequestType('markdown/statFile'); -export const readDirectoryRequestType: RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any> = new RequestType('markdown/readDirectory'); -export const findFilesRequestTypes = new RequestType<{}, string[], any>('markdown/findFiles'); +//#region From server +export const parse = new RequestType<{ uri: string }, Token[], any>('markdown/parse'); -// To server +export const fs_readFile = new RequestType<{ uri: string }, number[], any>('markdown/fs/readFile'); +export const fs_readDirectory = new RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any>('markdown/fs/readDirectory'); +export const fs_stat = new RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any>('markdown/fs/stat'); + +export const fs_watcher_create = new RequestType<{ id: number; uri: string; options: md.FileWatcherOptions }, void, any>('markdown/fs/watcher/create'); +export const fs_watcher_delete = new RequestType<{ id: number }, void, any>('markdown/fs/watcher/delete'); + +export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('markdown/findMarkdownFilesInWorkspace'); +//#endregion + +//#region To server export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace'); + +export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange'); +//#endregion diff --git a/extensions/markdown-language-features/src/test/diagnostic.test.ts b/extensions/markdown-language-features/src/test/diagnostic.test.ts deleted file mode 100644 index 020e745a2d3..00000000000 --- a/extensions/markdown-language-features/src/test/diagnostic.test.ts +++ /dev/null @@ -1,591 +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 'mocha'; -import * as vscode from 'vscode'; -import { DiagnosticCollectionReporter, DiagnosticComputer, DiagnosticConfiguration, DiagnosticLevel, DiagnosticManager, DiagnosticOptions, DiagnosticReporter } from '../languageFeatures/diagnostics'; -import { MdLinkProvider } from '../languageFeatures/documentLinks'; -import { MdReferencesProvider } from '../languageFeatures/references'; -import { MdTableOfContentsProvider } from '../tableOfContents'; -import { ITextDocument } from '../types/textDocument'; -import { noopToken } from '../util/cancellation'; -import { DisposableStore } from '../util/dispose'; -import { InMemoryDocument } from '../util/inMemoryDocument'; -import { ResourceMap } from '../util/resourceMap'; -import { IMdWorkspace } from '../workspace'; -import { createNewMarkdownEngine } from './engine'; -import { InMemoryMdWorkspace } from './inMemoryWorkspace'; -import { nulLogger } from './nulLogging'; -import { assertRangeEqual, joinLines, withStore, workspacePath } from './util'; - -const defaultDiagnosticsOptions = Object.freeze<DiagnosticOptions>({ - enabled: true, - validateFileLinks: DiagnosticLevel.warning, - validateMarkdownFileLinkFragments: undefined, - validateFragmentLinks: DiagnosticLevel.warning, - validateReferences: DiagnosticLevel.warning, - ignoreLinks: [], -}); - -async function getComputedDiagnostics(store: DisposableStore, doc: InMemoryDocument, workspace: IMdWorkspace, options: Partial<DiagnosticOptions> = {}): Promise<vscode.Diagnostic[]> { - const engine = createNewMarkdownEngine(); - const linkProvider = store.add(new MdLinkProvider(engine, workspace, nulLogger)); - const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger)); - const computer = new DiagnosticComputer(workspace, linkProvider, tocProvider); - return ( - await computer.getDiagnostics(doc, { ...defaultDiagnosticsOptions, ...options, }, noopToken) - ).diagnostics; -} - -function assertDiagnosticsEqual(actual: readonly vscode.Diagnostic[], expectedRanges: readonly vscode.Range[]) { - assert.strictEqual(actual.length, expectedRanges.length, "Diagnostic count equal"); - - for (let i = 0; i < actual.length; ++i) { - assertRangeEqual(actual[i].range, expectedRanges[i], `Range ${i} to be equal`); - } -} - -function orderDiagnosticsByRange(diagnostics: Iterable<vscode.Diagnostic>): readonly vscode.Diagnostic[] { - return Array.from(diagnostics).sort((a, b) => a.range.start.compareTo(b.range.start)); -} - -class MemoryDiagnosticConfiguration implements DiagnosticConfiguration { - - private readonly _onDidChange = new vscode.EventEmitter<void>(); - public readonly onDidChange = this._onDidChange.event; - - private _options: Partial<DiagnosticOptions>; - - constructor(options: Partial<DiagnosticOptions>) { - this._options = options; - } - - public getOptions(_resource: vscode.Uri): DiagnosticOptions { - return { - ...defaultDiagnosticsOptions, - ...this._options, - }; - } - - public update(newOptions: Partial<DiagnosticOptions>) { - this._options = newOptions; - this._onDidChange.fire(); - } -} - -class MemoryDiagnosticReporter extends DiagnosticReporter { - - private readonly diagnostics = new ResourceMap<readonly vscode.Diagnostic[]>(); - - constructor( - private readonly workspace: InMemoryMdWorkspace, - ) { - super(); - } - - override dispose(): void { - super.clear(); - this.clear(); - } - - override clear(): void { - super.clear(); - this.diagnostics.clear(); - } - - set(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]): void { - this.diagnostics.set(uri, diagnostics); - } - - isOpen(_uri: vscode.Uri): boolean { - return true; - } - - delete(uri: vscode.Uri): void { - this.diagnostics.delete(uri); - } - - get(uri: vscode.Uri): readonly vscode.Diagnostic[] { - return orderDiagnosticsByRange(this.diagnostics.get(uri) ?? []); - } - - getOpenDocuments(): ITextDocument[] { - return this.workspace.values(); - } -} - -suite('markdown: Diagnostic Computer', () => { - - test('Should not return any diagnostics for empty document', withStore(async (store) => { - const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines( - `text`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc])); - - const diagnostics = await getComputedDiagnostics(store, doc, workspace); - assert.deepStrictEqual(diagnostics, []); - })); - - test('Should generate diagnostic for link to file that does not exist', withStore(async (store) => { - const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines( - `[bad](/no/such/file.md)`, - `[good](/doc.md)`, - `[good-ref]: /doc.md`, - `[bad-ref]: /no/such/file.md`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc])); - - const diagnostics = await getComputedDiagnostics(store, doc, workspace); - assertDiagnosticsEqual(diagnostics, [ - new vscode.Range(0, 6, 0, 22), - new vscode.Range(3, 11, 3, 27), - ]); - })); - - test('Should generate diagnostics for links to header that does not exist in current file', withStore(async (store) => { - const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines( - `[good](#good-header)`, - `# Good Header`, - `[bad](#no-such-header)`, - `[good](#good-header)`, - `[good-ref]: #good-header`, - `[bad-ref]: #no-such-header`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc])); - - const diagnostics = await getComputedDiagnostics(store, doc, workspace); - assertDiagnosticsEqual(diagnostics, [ - new vscode.Range(2, 6, 2, 21), - new vscode.Range(5, 11, 5, 26), - ]); - })); - - test('Should generate diagnostics for links to non-existent headers in other files', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `# My header`, - `[good](#my-header)`, - `[good](/doc1.md#my-header)`, - `[good](doc1.md#my-header)`, - `[good](/doc2.md#other-header)`, - `[bad](/doc2.md#no-such-other-header)`, - )); - - const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines( - `# Other header`, - )); - - const diagnostics = await getComputedDiagnostics(store, doc1, new InMemoryMdWorkspace([doc1, doc2])); - assertDiagnosticsEqual(diagnostics, [ - new vscode.Range(5, 14, 5, 35), - ]); - })); - - test('Should support links both with and without .md file extension', withStore(async (store) => { - const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines( - `# My header`, - `[good](#my-header)`, - `[good](/doc.md#my-header)`, - `[good](doc.md#my-header)`, - `[good](/doc#my-header)`, - `[good](doc#my-header)`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc])); - - const diagnostics = await getComputedDiagnostics(store, doc, workspace); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('Should generate diagnostics for non-existent link reference', withStore(async (store) => { - const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines( - `[good link][good]`, - `[bad link][no-such]`, - ``, - `[good]: http://example.com`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc])); - - const diagnostics = await getComputedDiagnostics(store, doc, workspace); - assertDiagnosticsEqual(diagnostics, [ - new vscode.Range(1, 11, 1, 18), - ]); - })); - - test('Should not generate diagnostics when validate is disabled', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `[text](#no-such-header)`, - `[text][no-such-ref]`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc1])); - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, new MemoryDiagnosticConfiguration({ enabled: false }).getOptions(doc1.uri)); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('Should not generate diagnostics for email autolink', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `a <user@example.com> c`, - )); - - const diagnostics = await getComputedDiagnostics(store, doc1, new InMemoryMdWorkspace([doc1])); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('Should not generate diagnostics for html tag that looks like an autolink', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `a <tag>b</tag> c`, - `a <scope:tag>b</scope:tag> c`, - )); - - const diagnostics = await getComputedDiagnostics(store, doc1, new InMemoryMdWorkspace([doc1])); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('Should allow ignoring invalid file link using glob', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `[text](/no-such-file)`, - `![img](/no-such-file)`, - `[text]: /no-such-file`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc1])); - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/no-such-file'] }); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('Should be able to disable fragment validation for external files', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `![i](/doc2.md#no-such)`, - )); - const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines('')); - const workspace = new InMemoryMdWorkspace([doc1, doc2]); - - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { validateMarkdownFileLinkFragments: DiagnosticLevel.ignore }); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('Disabling own fragment validation should also disable path fragment validation by default', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `[b](#no-head)`, - `![i](/doc2.md#no-such)`, - )); - const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines('')); - const workspace = new InMemoryMdWorkspace([doc1, doc2]); - - { - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { validateFragmentLinks: DiagnosticLevel.ignore }); - assertDiagnosticsEqual(diagnostics, []); - } - { - // But we should be able to override the default - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { validateFragmentLinks: DiagnosticLevel.ignore, validateMarkdownFileLinkFragments: DiagnosticLevel.warning }); - assertDiagnosticsEqual(diagnostics, [ - new vscode.Range(1, 13, 1, 21), - ]); - } - })); - - test('ignoreLinks should allow skipping link to non-existent file', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `[text](/no-such-file#header)`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc1])); - - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/no-such-file'] }); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('ignoreLinks should not consider link fragment', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `[text](/no-such-file#header)`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc1])); - - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/no-such-file'] }); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('ignoreLinks should support globs', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `![i](/images/aaa.png)`, - `![i](/images/sub/bbb.png)`, - `![i](/images/sub/sub2/ccc.png)`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc1])); - - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/images/**/*.png'] }); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('ignoreLinks should support ignoring header', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `![i](#no-such)`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc1])); - - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['#no-such'] }); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('ignoreLinks should support ignoring header in file', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `![i](/doc2.md#no-such)`, - )); - const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines('')); - const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2])); - - { - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/doc2.md#no-such'] }); - assertDiagnosticsEqual(diagnostics, []); - } - { - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/doc2.md#*'] }); - assertDiagnosticsEqual(diagnostics, []); - } - })); - - test('ignoreLinks should support ignore header links if file is ignored', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `![i](/doc2.md#no-such)`, - )); - const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines('')); - const workspace = new InMemoryMdWorkspace([doc1, doc2]); - - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/doc2.md'] }); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('Should not detect checkboxes as invalid links', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `- [x]`, - `- [X]`, - `- [ ]`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc1])); - - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/doc2.md'] }); - assertDiagnosticsEqual(diagnostics, []); - })); - - test('Should detect invalid links with titles', withStore(async (store) => { - const doc = new InMemoryDocument(workspacePath('doc1.md'), joinLines( - `[link](<no such.md> "text")`, - `[link](<no such.md> 'text')`, - `[link](<no such.md> (text))`, - `[link](no-such.md "text")`, - `[link](no-such.md 'text')`, - `[link](no-such.md (text))`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc])); - - const diagnostics = await getComputedDiagnostics(store, doc, workspace); - assertDiagnosticsEqual(diagnostics, [ - new vscode.Range(0, 8, 0, 18), - new vscode.Range(1, 8, 1, 18), - new vscode.Range(2, 8, 2, 18), - new vscode.Range(3, 7, 3, 17), - new vscode.Range(4, 7, 4, 17), - new vscode.Range(5, 7, 5, 17), - ]); - })); - - test('Should generate diagnostics for non-existent header using file link to own file', withStore(async (store) => { - const doc = new InMemoryDocument(workspacePath('sub', 'doc.md'), joinLines( - `[bad](doc.md#no-such)`, - `[bad](doc#no-such)`, - `[bad](/sub/doc.md#no-such)`, - `[bad](/sub/doc#no-such)`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc])); - - const diagnostics = await getComputedDiagnostics(store, doc, workspace); - assertDiagnosticsEqual(orderDiagnosticsByRange(diagnostics), [ - new vscode.Range(0, 12, 0, 20), - new vscode.Range(1, 9, 1, 17), - new vscode.Range(2, 17, 2, 25), - new vscode.Range(3, 14, 3, 22), - ]); - })); - - test('Own header link using file path link should be controlled by "validateMarkdownFileLinkFragments" instead of "validateFragmentLinks"', withStore(async (store) => { - const doc1 = new InMemoryDocument(workspacePath('sub', 'doc.md'), joinLines( - `[bad](doc.md#no-such)`, - `[bad](doc#no-such)`, - `[bad](/sub/doc.md#no-such)`, - `[bad](/sub/doc#no-such)`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc1])); - - const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { - validateFragmentLinks: DiagnosticLevel.ignore, - validateMarkdownFileLinkFragments: DiagnosticLevel.warning, - }); - assertDiagnosticsEqual(orderDiagnosticsByRange(diagnostics), [ - new vscode.Range(0, 12, 0, 20), - new vscode.Range(1, 9, 1, 17), - new vscode.Range(2, 17, 2, 25), - new vscode.Range(3, 14, 3, 22), - ]); - })); -}); - -suite('Markdown: Diagnostics manager', () => { - - function createDiagnosticsManager( - store: DisposableStore, - workspace: IMdWorkspace, - configuration = new MemoryDiagnosticConfiguration({}), - reporter: DiagnosticReporter = new DiagnosticCollectionReporter(), - ) { - const engine = createNewMarkdownEngine(); - const linkProvider = store.add(new MdLinkProvider(engine, workspace, nulLogger)); - const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger)); - const referencesProvider = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger)); - const manager = store.add(new DiagnosticManager( - workspace, - new DiagnosticComputer(workspace, linkProvider, tocProvider), - configuration, - reporter, - referencesProvider, - tocProvider, - nulLogger, - 0)); - return manager; - } - - test('Changing enable/disable should recompute diagnostics', withStore(async (store) => { - const doc1Uri = workspacePath('doc1.md'); - const doc2Uri = workspacePath('doc2.md'); - const workspace = store.add(new InMemoryMdWorkspace([ - new InMemoryDocument(doc1Uri, joinLines( - `[text](#no-such-1)`, - )), - new InMemoryDocument(doc2Uri, joinLines( - `[text](#no-such-2)`, - )) - ])); - - const reporter = store.add(new MemoryDiagnosticReporter(workspace)); - const config = new MemoryDiagnosticConfiguration({ enabled: true }); - - const manager = createDiagnosticsManager(store, workspace, config, reporter); - await manager.ready; - - // Check initial state (Enabled) - await reporter.waitPendingWork(); - assertDiagnosticsEqual(reporter.get(doc1Uri), [ - new vscode.Range(0, 7, 0, 17), - ]); - assertDiagnosticsEqual(reporter.get(doc2Uri), [ - new vscode.Range(0, 7, 0, 17), - ]); - - // Disable - config.update({ enabled: false }); - await reporter.waitPendingWork(); - assertDiagnosticsEqual(reporter.get(doc1Uri), []); - assertDiagnosticsEqual(reporter.get(doc2Uri), []); - - // Enable - config.update({ enabled: true }); - await reporter.waitPendingWork(); - assertDiagnosticsEqual(reporter.get(doc1Uri), [ - new vscode.Range(0, 7, 0, 17), - ]); - assertDiagnosticsEqual(reporter.get(doc2Uri), [ - new vscode.Range(0, 7, 0, 17), - ]); - })); - - test('Should revalidate linked files when header changes', withStore(async (store) => { - const doc1Uri = workspacePath('doc1.md'); - const doc1 = new InMemoryDocument(doc1Uri, joinLines( - `[text](#no-such)`, - `[text](/doc2.md#header)`, - )); - const doc2Uri = workspacePath('doc2.md'); - const doc2 = new InMemoryDocument(doc2Uri, joinLines( - `# Header`, - `[text](#header)`, - `[text](#no-such-2)`, - )); - const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2])); - const reporter = store.add(new MemoryDiagnosticReporter(workspace)); - - const manager = createDiagnosticsManager(store, workspace, new MemoryDiagnosticConfiguration({}), reporter); - await manager.ready; - - // Check initial state - await reporter.waitPendingWork(); - assertDiagnosticsEqual(reporter.get(doc1Uri), [ - new vscode.Range(0, 7, 0, 15), - ]); - assertDiagnosticsEqual(reporter.get(doc2Uri), [ - new vscode.Range(2, 7, 2, 17), - ]); - - // Edit header - workspace.updateDocument(new InMemoryDocument(doc2Uri, joinLines( - `# new header`, - `[text](#new-header)`, - `[text](#no-such-2)`, - ))); - await reporter.waitPendingWork(); - assertDiagnosticsEqual(reporter.get(doc1Uri), [ - new vscode.Range(0, 7, 0, 15), - new vscode.Range(1, 15, 1, 22), - ]); - assertDiagnosticsEqual(reporter.get(doc2Uri), [ - new vscode.Range(2, 7, 2, 17), - ]); - - // Revert to original file - workspace.updateDocument(new InMemoryDocument(doc2Uri, joinLines( - `# header`, - `[text](#header)`, - `[text](#no-such-2)`, - ))); - await reporter.waitPendingWork(); - assertDiagnosticsEqual(reporter.get(doc1Uri), [ - new vscode.Range(0, 7, 0, 15) - ]); - assertDiagnosticsEqual(reporter.get(doc2Uri), [ - new vscode.Range(2, 7, 2, 17), - ]); - })); - - test('Should revalidate linked files when file is deleted/created', withStore(async (store) => { - const doc1Uri = workspacePath('doc1.md'); - const doc1 = new InMemoryDocument(doc1Uri, joinLines( - `[text](/doc2.md)`, - `[text](/doc2.md#header)`, - )); - const doc2Uri = workspacePath('doc2.md'); - const doc2 = new InMemoryDocument(doc2Uri, joinLines( - `# Header` - )); - const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2])); - const reporter = store.add(new MemoryDiagnosticReporter(workspace)); - - const manager = createDiagnosticsManager(store, workspace, new MemoryDiagnosticConfiguration({}), reporter); - await manager.ready; - - // Check initial state - await reporter.waitPendingWork(); - assertDiagnosticsEqual(reporter.get(doc1Uri), []); - - // Edit header - workspace.deleteDocument(doc2Uri); - - await reporter.waitPendingWork(); - assertDiagnosticsEqual(reporter.get(doc1Uri), [ - new vscode.Range(0, 7, 0, 15), - new vscode.Range(1, 7, 1, 22), - ]); - - // Revert to original file - workspace.createDocument(doc2); - await reporter.waitPendingWork(); - assertDiagnosticsEqual(reporter.get(doc1Uri), []); - })); -}); diff --git a/extensions/markdown-language-features/src/test/documentInfoCache.test.ts b/extensions/markdown-language-features/src/test/documentInfoCache.test.ts deleted file mode 100644 index a1b291aee0b..00000000000 --- a/extensions/markdown-language-features/src/test/documentInfoCache.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import 'mocha'; -import { InMemoryDocument } from '../util/inMemoryDocument'; -import { MdDocumentInfoCache } from '../util/workspaceCache'; -import { InMemoryMdWorkspace } from './inMemoryWorkspace'; -import { workspacePath } from './util'; - -suite('DocumentInfoCache', () => { - test('Repeated calls should only compute value once', async () => { - const doc = workspacePath('doc.md'); - const workspace = new InMemoryMdWorkspace([ - new InMemoryDocument(doc, '') - ]); - - let i = 0; - const cache = new MdDocumentInfoCache<number>(workspace, async () => { - return ++i; - }); - - const a = cache.get(doc); - const b = cache.get(doc); - - assert.strictEqual(await a, 1); - assert.strictEqual(i, 1); - assert.strictEqual(await b, 1); - assert.strictEqual(i, 1); - }); -}); diff --git a/extensions/markdown-language-features/src/test/tableOfContentsProvider.test.ts b/extensions/markdown-language-features/src/test/tableOfContentsProvider.test.ts deleted file mode 100644 index 226ca4437e0..00000000000 --- a/extensions/markdown-language-features/src/test/tableOfContentsProvider.test.ts +++ /dev/null @@ -1,136 +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 'mocha'; -import * as vscode from 'vscode'; -import { TableOfContents } from '../tableOfContents'; -import { ITextDocument } from '../types/textDocument'; -import { InMemoryDocument } from '../util/inMemoryDocument'; -import { createNewMarkdownEngine } from './engine'; - - -const testFileName = vscode.Uri.file('test.md'); - -function createToc(doc: ITextDocument): Promise<TableOfContents> { - const engine = createNewMarkdownEngine(); - return TableOfContents.create(engine, doc); -} - -suite('markdown.TableOfContentsProvider', () => { - test('Lookup should not return anything for empty document', async () => { - const doc = new InMemoryDocument(testFileName, ''); - const provider = await createToc(doc); - - assert.strictEqual(provider.lookup(''), undefined); - assert.strictEqual(provider.lookup('foo'), undefined); - }); - - test('Lookup should not return anything for document with no headers', async () => { - const doc = new InMemoryDocument(testFileName, 'a *b*\nc'); - const provider = await createToc(doc); - - assert.strictEqual(provider.lookup(''), undefined); - assert.strictEqual(provider.lookup('foo'), undefined); - assert.strictEqual(provider.lookup('a'), undefined); - assert.strictEqual(provider.lookup('b'), undefined); - }); - - test('Lookup should return basic #header', async () => { - const doc = new InMemoryDocument(testFileName, `# a\nx\n# c`); - const provider = await createToc(doc); - - { - const entry = provider.lookup('a'); - assert.ok(entry); - assert.strictEqual(entry!.line, 0); - } - { - assert.strictEqual(provider.lookup('x'), undefined); - } - { - const entry = provider.lookup('c'); - assert.ok(entry); - assert.strictEqual(entry!.line, 2); - } - }); - - test('Lookups should be case in-sensitive', async () => { - const doc = new InMemoryDocument(testFileName, `# fOo\n`); - const provider = await createToc(doc); - - assert.strictEqual((provider.lookup('fOo'))!.line, 0); - assert.strictEqual((provider.lookup('foo'))!.line, 0); - assert.strictEqual((provider.lookup('FOO'))!.line, 0); - }); - - test('Lookups should ignore leading and trailing white-space, and collapse internal whitespace', async () => { - const doc = new InMemoryDocument(testFileName, `# f o o \n`); - const provider = await createToc(doc); - - assert.strictEqual((provider.lookup('f o o'))!.line, 0); - assert.strictEqual((provider.lookup(' f o o'))!.line, 0); - assert.strictEqual((provider.lookup(' f o o '))!.line, 0); - assert.strictEqual((provider.lookup('f o o'))!.line, 0); - assert.strictEqual((provider.lookup('f o o'))!.line, 0); - - assert.strictEqual(provider.lookup('f'), undefined); - assert.strictEqual(provider.lookup('foo'), undefined); - assert.strictEqual(provider.lookup('fo o'), undefined); - }); - - test('should handle special characters #44779', async () => { - const doc = new InMemoryDocument(testFileName, `# Indentação\n`); - const provider = await createToc(doc); - - assert.strictEqual((provider.lookup('indentação'))!.line, 0); - }); - - test('should handle special characters 2, #48482', async () => { - const doc = new InMemoryDocument(testFileName, `# Инструкция - Делай Раз, Делай Два\n`); - const provider = await createToc(doc); - - assert.strictEqual((provider.lookup('инструкция---делай-раз-делай-два'))!.line, 0); - }); - - test('should handle special characters 3, #37079', async () => { - const doc = new InMemoryDocument(testFileName, `## Header 2 -### Header 3 -## Заголовок 2 -### Заголовок 3 -### Заголовок Header 3 -## Заголовок`); - - const provider = await createToc(doc); - - assert.strictEqual((provider.lookup('header-2'))!.line, 0); - assert.strictEqual((provider.lookup('header-3'))!.line, 1); - assert.strictEqual((provider.lookup('Заголовок-2'))!.line, 2); - assert.strictEqual((provider.lookup('Заголовок-3'))!.line, 3); - assert.strictEqual((provider.lookup('Заголовок-header-3'))!.line, 4); - assert.strictEqual((provider.lookup('Заголовок'))!.line, 5); - }); - - test('Lookup should support suffixes for repeated headers', async () => { - const doc = new InMemoryDocument(testFileName, `# a\n# a\n## a`); - const provider = await createToc(doc); - - { - const entry = provider.lookup('a'); - assert.ok(entry); - assert.strictEqual(entry!.line, 0); - } - { - const entry = provider.lookup('a-1'); - assert.ok(entry); - assert.strictEqual(entry!.line, 1); - } - { - const entry = provider.lookup('a-2'); - assert.ok(entry); - assert.strictEqual(entry!.line, 2); - } - }); -}); diff --git a/extensions/markdown-language-features/src/util/tableOfContentsWatcher.ts b/extensions/markdown-language-features/src/util/tableOfContentsWatcher.ts deleted file mode 100644 index 8bbbf26fafb..00000000000 --- a/extensions/markdown-language-features/src/util/tableOfContentsWatcher.ts +++ /dev/null @@ -1,89 +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 { MdTableOfContentsProvider, TableOfContents } from '../tableOfContents'; -import { ITextDocument } from '../types/textDocument'; -import { IMdWorkspace } from '../workspace'; -import { equals } from './arrays'; -import { Delayer } from './async'; -import { Disposable } from './dispose'; -import { ResourceMap } from './resourceMap'; - -/** - * Check if the items in a table of contents have changed. - * - * This only checks for changes in the entries themselves, not for any changes in their locations. - */ -function hasTableOfContentsChanged(a: TableOfContents, b: TableOfContents): boolean { - const aSlugs = a.entries.map(entry => entry.slug.value).sort(); - const bSlugs = b.entries.map(entry => entry.slug.value).sort(); - return !equals(aSlugs, bSlugs); -} - -export class MdTableOfContentsWatcher extends Disposable { - - private readonly _files = new ResourceMap<{ - readonly toc: TableOfContents; - }>(); - - private readonly _pending = new ResourceMap<void>(); - - private readonly _onTocChanged = this._register(new vscode.EventEmitter<{ readonly uri: vscode.Uri }>); - public readonly onTocChanged = this._onTocChanged.event; - - private readonly delayer: Delayer<void>; - - public constructor( - private readonly workspace: IMdWorkspace, - private readonly tocProvider: MdTableOfContentsProvider, - private readonly delay: number, - ) { - super(); - - this.delayer = this._register(new Delayer<void>(delay)); - - this._register(this.workspace.onDidChangeMarkdownDocument(this.onDidChangeDocument, this)); - this._register(this.workspace.onDidCreateMarkdownDocument(this.onDidCreateDocument, this)); - this._register(this.workspace.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this)); - } - - private async onDidCreateDocument(document: ITextDocument) { - const toc = await this.tocProvider.getForDocument(document); - this._files.set(document.uri, { toc }); - } - - private async onDidChangeDocument(document: ITextDocument) { - if (this.delay > 0) { - this._pending.set(document.uri); - this.delayer.trigger(() => this.flushPending()); - } else { - this.updateForResource(document.uri); - } - } - - private onDidDeleteDocument(resource: vscode.Uri) { - this._files.delete(resource); - this._pending.delete(resource); - } - - private async flushPending() { - const pending = [...this._pending.keys()]; - this._pending.clear(); - - return Promise.all(pending.map(resource => this.updateForResource(resource))); - } - - private async updateForResource(resource: vscode.Uri) { - const existing = this._files.get(resource); - const newToc = await this.tocProvider.get(resource); - - if (!existing || hasTableOfContentsChanged(existing.toc, newToc)) { - this._onTocChanged.fire({ uri: resource }); - } - - this._files.set(resource, { toc: newToc }); - } -} diff --git a/extensions/markdown-language-features/src/util/workspaceCache.ts b/extensions/markdown-language-features/src/util/workspaceCache.ts index 8432675c87a..2569dbee2b4 100644 --- a/extensions/markdown-language-features/src/util/workspaceCache.ts +++ b/extensions/markdown-language-features/src/util/workspaceCache.ts @@ -114,74 +114,3 @@ export class MdDocumentInfoCache<T> extends Disposable { this._cache.delete(resource); } } - -/** - * Cache of information across all markdown files in the workspace. - * - * Unlike {@link MdDocumentInfoCache}, the entries here are computed eagerly for every file in the workspace. - * However the computation of the values is still lazy. - */ -export class MdWorkspaceInfoCache<T> extends Disposable { - - private readonly _cache = new LazyResourceMap<T>(); - private _init?: Promise<void>; - - public constructor( - private readonly workspace: IMdWorkspace, - private readonly getValue: (document: ITextDocument) => Promise<T>, - ) { - super(); - } - - public async entries(): Promise<Array<[vscode.Uri, T]>> { - await this.ensureInit(); - return this._cache.entries(); - } - - public async values(): Promise<Array<T>> { - await this.ensureInit(); - return Array.from(await this._cache.entries(), x => x[1]); - } - - public async getForDocs(docs: readonly ITextDocument[]): Promise<T[]> { - for (const doc of docs) { - if (!this._cache.has(doc.uri)) { - this.update(doc); - } - } - - return Promise.all(docs.map(doc => this._cache.get(doc.uri) as Promise<T>)); - } - - private async ensureInit(): Promise<void> { - if (!this._init) { - this._init = this.populateCache(); - - this._register(this.workspace.onDidChangeMarkdownDocument(this.onDidChangeDocument, this)); - this._register(this.workspace.onDidCreateMarkdownDocument(this.onDidChangeDocument, this)); - this._register(this.workspace.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this)); - } - await this._init; - } - - private async populateCache(): Promise<void> { - const markdownDocumentUris = await this.workspace.getAllMarkdownDocuments(); - for (const document of markdownDocumentUris) { - if (!this._cache.has(document.uri)) { - this.update(document); - } - } - } - - private update(document: ITextDocument): void { - this._cache.set(document.uri, lazy(() => this.getValue(document))); - } - - private onDidChangeDocument(document: ITextDocument) { - this.update(document); - } - - private onDidDeleteDocument(resource: vscode.Uri) { - this._cache.delete(resource); - } -} diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 12c6af02b5f..3150da7a642 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -237,21 +237,42 @@ vscode-languageserver-textdocument@^1.0.4: resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz#3cd56dd14cec1d09e86c4bb04b09a246cb3df157" integrity sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ== +vscode-languageserver-textdocument@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.5.tgz#838769940ece626176ec5d5a2aa2d0aa69f5095c" + integrity sha512-1ah7zyQjKBudnMiHbZmxz5bYNM9KKZYz+5VQLj+yr8l+9w3g+WAhCkUkWbhMEdC5u0ub4Ndiye/fDyS8ghIKQg== + vscode-languageserver-types@3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz#c2d87fa7784f8cac389deb3ff1e2d9a7bef07e16" integrity sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ== -vscode-languageserver-types@^3.17.2: +vscode-languageserver-types@^3.17.1, vscode-languageserver-types@^3.17.2: version "3.17.2" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz#b2c2e7de405ad3d73a883e91989b850170ffc4f2" integrity sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA== +vscode-markdown-languageservice@^0.0.0-alpha.10: + version "0.0.0-alpha.10" + resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.0.0-alpha.10.tgz#53b69c981eed7fd5efa155ab8c0f169995568681" + integrity sha512-rJ85nJ+d45yCz9lBhipavoWXz/vW5FknqqUpLqhe3/2xkrhxt8zcekhSoDepgkKFcTORAFV6g1SnnqxbVhX+uA== + dependencies: + picomatch "^2.3.1" + vscode-languageserver-textdocument "^1.0.5" + vscode-languageserver-types "^3.17.1" + vscode-nls "^5.0.1" + vscode-uri "^3.0.3" + vscode-nls@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== +vscode-nls@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.1.tgz#ba23fc4d4420d25e7f886c8e83cbdcec47aa48b2" + integrity sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A== + vscode-uri@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 0585285b6d7..b09593f6c59 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -554,8 +554,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType } // Reconfigure any plugins - for (const [config, pluginName] of this.pluginManager.configurations()) { - this.configurePlugin(config, pluginName); + for (const [pluginName, config] of this.pluginManager.configurations()) { + this.configurePlugin(pluginName, config); } } 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 88eb9d8a2bb..5ffcebf945c 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -258,7 +258,7 @@ const apiTestContentProvider: vscode.NotebookContentProvider = { // }); }); - test('edit API batch edits', async function () { + test.skip('edit API batch edits', async function () { // TODO@rebornix https://github.com/microsoft/vscode/issues/155808 const notebook = await openRandomNotebookDocument(); const editor = await vscode.window.showNotebookDocument(notebook); @@ -284,7 +284,7 @@ const apiTestContentProvider: vscode.NotebookContentProvider = { assert.ok(cell.metadata.extraCellMetadata, `Test cell metdata not found`); }); - test('edit API batch edits undo/redo', async function () { + test.skip('edit API batch edits undo/redo', async function () { // TODO@rebornix https://github.com/microsoft/vscode/issues/155825 const notebook = await openRandomNotebookDocument(); const editor = await vscode.window.showNotebookDocument(notebook); diff --git a/package.json b/package.json index 5a345056c0e..f15864fabdc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.70.0", - "distro": "1a72c46622967eab6ea48516a2153c55d7e18e53", + "distro": "990065ff739688c0d4ad94e172eeccd6bd5ef124", "author": { "name": "Microsoft Corporation" }, @@ -56,7 +56,8 @@ "minify-vscode-reh-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode-reh-web", "hygiene": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js hygiene", "core-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js core-ci", - "extensions-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci" + "extensions-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci", + "webview-generate-csp-hash": "npx github:apaatsio/csp-hash-from-html csp-hash ./src/vs/workbench/contrib/webview/browser/pre/index.html" }, "dependencies": { "@microsoft/1ds-core-js": "^3.2.2", @@ -85,12 +86,12 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "4.20.0-beta.13", - "xterm-addon-search": "0.10.0-beta.2", - "xterm-addon-serialize": "0.8.0-beta.2", + "xterm": "4.20.0-beta.20", + "xterm-addon-search": "0.10.0-beta.3", + "xterm-addon-serialize": "0.8.0-beta.3", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.13.0-beta.7", - "xterm-headless": "4.20.0-beta.13", + "xterm-addon-webgl": "0.13.0-beta.9", + "xterm-headless": "4.20.0-beta.20", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, @@ -124,7 +125,7 @@ "@typescript-eslint/eslint-plugin": "^5.10.0", "@typescript-eslint/parser": "^5.10.0", "@vscode/telemetry-extractor": "^1.9.6", - "@vscode/test-web": "^0.0.22", + "@vscode/test-web": "^0.0.29", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", @@ -199,7 +200,7 @@ "style-loader": "^1.3.0", "ts-loader": "^9.2.7", "tsec": "0.1.4", - "typescript": "^4.8.0-dev.20220711", + "typescript": "^4.8.0-dev.20220719", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", diff --git a/remote/package.json b/remote/package.json index 10c685468d1..6b5622069ed 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.20.0-beta.13", - "xterm-addon-search": "0.10.0-beta.2", - "xterm-addon-serialize": "0.8.0-beta.2", + "xterm": "4.20.0-beta.20", + "xterm-addon-search": "0.10.0-beta.3", + "xterm-addon-serialize": "0.8.0-beta.3", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.13.0-beta.7", - "xterm-headless": "4.20.0-beta.13", + "xterm-addon-webgl": "0.13.0-beta.9", + "xterm-headless": "4.20.0-beta.20", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index b44711e8c37..9e4dc583566 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -11,9 +11,9 @@ "tas-client-umd": "0.1.6", "vscode-oniguruma": "1.6.1", "vscode-textmate": "7.0.1", - "xterm": "4.20.0-beta.13", - "xterm-addon-search": "0.10.0-beta.2", + "xterm": "4.20.0-beta.20", + "xterm-addon-search": "0.10.0-beta.3", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.13.0-beta.7" + "xterm-addon-webgl": "0.13.0-beta.9" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index ba145029748..a2ae83d5fce 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -68,22 +68,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.10.0-beta.2: - version "0.10.0-beta.2" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.2.tgz#a937d1e9a70fde8eeb7d1df485039b2d5fc1d707" - integrity sha512-ybafAbX9V4sfkzmUsWmtfEYExG8jj73bTF9pEa/Lhd5q4bviW4LcFaw/n3lKHn/1tSgSVgzoD13u1ZaZR78SfQ== +xterm-addon-search@0.10.0-beta.3: + version "0.10.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.3.tgz#5194434d86105637c71f6f20139a9d0b5c1a956a" + integrity sha512-UeGm/ymnp7HUYJJtsP0D+bljOWbdk3MctcLJ+0jv8AmU6YlAzJFtouvYSQrD5SAMyht5CRsvjzFgqic9X02JYg== 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.13.0-beta.7: - version "0.13.0-beta.7" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.7.tgz#9b514fa9792ced755c8321ddd06529f9912db2a1" - integrity sha512-U3sKzkziZRwb13MyNp0eMwt5BWyM938epAv0ZOnI5Vjq06S2naS37GgO/FY+0eu5wuBKkZhT9lNDv7n8rMqd+w== +xterm-addon-webgl@0.13.0-beta.9: + version "0.13.0-beta.9" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.9.tgz#66a9ac142ae347d0548abbf4e66bb2f35f415adb" + integrity sha512-x1o1tpCqIsICvhcRsZs+BLcwUIdizYS2G4TIH0KBnUDiSN+oSqpVBQNG8qKg56xbK8WtpdbQ9dLB7JR2W5cX0g== -xterm@4.20.0-beta.13: - version "4.20.0-beta.13" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.13.tgz#7526e19310daa56a11c26eb81a2706593c1378ec" - integrity sha512-ovXGhvM/qDRNGlGO653WY1pxIZrnI4+ayVM7pCnxO/tRwUIym6LGS3NurYrhGYrXOjoLJZxQ4EjToAfHvAg2Gg== +xterm@4.20.0-beta.20: + version "4.20.0-beta.20" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.20.tgz#2979a31839f7b8ee3ffe4f063b40c02facdb0fed" + integrity sha512-ltDtTquH+33tXQPFSDqenbgz6LkvIob6l6Rac85L4aX5Ve7P3ubVLrq+lTFJGQn3iiwGqNmnE1t1EUuGhxsXcQ== diff --git a/remote/yarn.lock b/remote/yarn.lock index 3076135dad1..762745d5927 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -788,35 +788,35 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xterm-addon-search@0.10.0-beta.2: - version "0.10.0-beta.2" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.2.tgz#a937d1e9a70fde8eeb7d1df485039b2d5fc1d707" - integrity sha512-ybafAbX9V4sfkzmUsWmtfEYExG8jj73bTF9pEa/Lhd5q4bviW4LcFaw/n3lKHn/1tSgSVgzoD13u1ZaZR78SfQ== +xterm-addon-search@0.10.0-beta.3: + version "0.10.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.3.tgz#5194434d86105637c71f6f20139a9d0b5c1a956a" + integrity sha512-UeGm/ymnp7HUYJJtsP0D+bljOWbdk3MctcLJ+0jv8AmU6YlAzJFtouvYSQrD5SAMyht5CRsvjzFgqic9X02JYg== -xterm-addon-serialize@0.8.0-beta.2: - version "0.8.0-beta.2" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0-beta.2.tgz#f30656d4ff1570ac105bacffe443385666654598" - integrity sha512-IDaRxO1zwjF9fDJp6u27Lv8852kEZ0HlbB0wLZbcIGZxDuPDLfvw8s/BV7f6MFB+mZq19CjyHGH4oPzZkc0rLQ== +xterm-addon-serialize@0.8.0-beta.3: + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0-beta.3.tgz#47ade3fedacbb75bd26e63cfe0120586623e0e4f" + integrity sha512-gvfempZCYuAhLqN4O6fA2TuoavPjOxFKlh8hLcOzPackiLUhwKr1jQpDXcnq8VgqUiGgb+XNZpPEbI0Q7EhTgA== 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.13.0-beta.7: - version "0.13.0-beta.7" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.7.tgz#9b514fa9792ced755c8321ddd06529f9912db2a1" - integrity sha512-U3sKzkziZRwb13MyNp0eMwt5BWyM938epAv0ZOnI5Vjq06S2naS37GgO/FY+0eu5wuBKkZhT9lNDv7n8rMqd+w== +xterm-addon-webgl@0.13.0-beta.9: + version "0.13.0-beta.9" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.9.tgz#66a9ac142ae347d0548abbf4e66bb2f35f415adb" + integrity sha512-x1o1tpCqIsICvhcRsZs+BLcwUIdizYS2G4TIH0KBnUDiSN+oSqpVBQNG8qKg56xbK8WtpdbQ9dLB7JR2W5cX0g== -xterm-headless@4.20.0-beta.13: - version "4.20.0-beta.13" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.13.tgz#a7d9d8837e3f78e106006cc94cf63ec13a9fd991" - integrity sha512-y4YI+Ogv2R2I++tsyvx5Q7csAaN7mG2yMMMBb/u4dXnrFmSGYs/R8ZFkeHgAW4Ju4uI3Rizb+ZdwtN1uG043Rw== +xterm-headless@4.20.0-beta.20: + version "4.20.0-beta.20" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.20.tgz#da2d8131b02d6f1e37f47cc17e578f2c2980fbb6" + integrity sha512-JK4jUIiUH7TdzvMrpfDnbGxTuC4s7byjqnMHR8+gIpY8qCFjz0xcMFSbp+ZshxGwVyziI4jtJqTHZjFToT2/kw== -xterm@4.20.0-beta.13: - version "4.20.0-beta.13" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.13.tgz#7526e19310daa56a11c26eb81a2706593c1378ec" - integrity sha512-ovXGhvM/qDRNGlGO653WY1pxIZrnI4+ayVM7pCnxO/tRwUIym6LGS3NurYrhGYrXOjoLJZxQ4EjToAfHvAg2Gg== +xterm@4.20.0-beta.20: + version "4.20.0-beta.20" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.20.tgz#2979a31839f7b8ee3ffe4f063b40c02facdb0fed" + integrity sha512-ltDtTquH+33tXQPFSDqenbgz6LkvIob6l6Rac85L4aX5Ve7P3ubVLrq+lTFJGQn3iiwGqNmnE1t1EUuGhxsXcQ== yallist@^4.0.0: version "4.0.0" diff --git a/resources/linux/debian/control.template b/resources/linux/debian/control.template index bfe0aa52da2..1a5981bb219 100644 --- a/resources/linux/debian/control.template +++ b/resources/linux/debian/control.template @@ -1,7 +1,8 @@ Package: @@NAME@@ Version: @@VERSION@@ Section: devel -Depends: libnss3 (>= 2:3.26), gnupg, apt, libxkbfile1, libsecret-1-0, libgtk-3-0 (>= 3.10.0), libxss1, libgbm1 +Depends: @@DEPENDS@@ +Recommends: @@RECOMMENDS@@ Priority: optional Architecture: @@ARCHITECTURE@@ Maintainer: Microsoft Corporation <vscode-linux@microsoft.com> diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 4db47cd947a..4019ea90b61 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1738,56 +1738,81 @@ type HTMLElementAttributeKeys<T> = Partial<{ [K in keyof T]: T[K] extends Functi type ElementAttributes<T> = HTMLElementAttributeKeys<T> & Record<string, any>; type RemoveHTMLElement<T> = T extends HTMLElement ? never : T; type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; -type ArrayToObj<T extends any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>; - -type TagToElement<T> = T extends `.${string}` - ? HTMLDivElement - : T extends `#${string}` - ? HTMLDivElement - : T extends `${infer TStart}#${string}` - ? TStart extends keyof HTMLElementTagNameMap - ? HTMLElementTagNameMap[TStart] +type ArrayToObj<T extends readonly any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>; +type HHTMLElementTagNameMap = HTMLElementTagNameMap & { '': HTMLDivElement }; + +type TagToElement<T> = T extends `${infer TStart}#${string}` + ? TStart extends keyof HHTMLElementTagNameMap + ? HHTMLElementTagNameMap[TStart] : HTMLElement : T extends `${infer TStart}.${string}` - ? TStart extends keyof HTMLElementTagNameMap - ? HTMLElementTagNameMap[TStart] + ? TStart extends keyof HHTMLElementTagNameMap + ? HHTMLElementTagNameMap[TStart] : HTMLElement : T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : HTMLElement; +type TagToElementAndId<TTag> = TTag extends `${infer TTag}@${infer TId}` + ? { element: TagToElement<TTag>; id: TId } + : { element: TagToElement<TTag>; id: 'root' }; + +type TagToRecord<TTag> = TagToElementAndId<TTag> extends { element: infer TElement; id: infer TId } + ? Record<(TId extends string ? TId : never) | 'root', TElement> + : never; + +type Child = HTMLElement | string | Record<string, HTMLElement>; +type Children = [] + | [Child] + | [Child, Child] + | [Child, Child, Child] + | [Child, Child, Child, Child] + | [Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child] + | [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]; + +const H_REGEX = /(?<tag>[\w\-]+)?(?:#(?<id>[\w\-]+))?(?<class>(?:\.(?:[\w\-]+))*)(?:@(?<name>(?:[\w\_])+))?/; + /** * A helper function to create nested dom nodes. * * * ```ts - * private readonly htmlElements = h('div.code-view', [ - * h('div.title', { $: 'title' }), + * const elements = h('div.code-view', [ + * h('div.title@title'), * h('div.container', [ - * h('div.gutter', { $: 'gutterDiv' }), - * h('div', { $: 'editor' }), + * h('div.gutter@gutterDiv'), + * h('div@editor'), * ]), * ]); - * private readonly editor = createEditor(this.htmlElements.editor); + * const editor = createEditor(elements.editor); * ``` */ -export function h<TTag extends string>( - tag: TTag -): (Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; -export function h<TTag extends string, TId extends string>( - tag: TTag, - attributes: { $: TId } & Partial<ElementAttributes<TagToElement<TTag>>> -): Record<TId | 'root', TagToElement<TTag>>; -export function h<TTag extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>( - tag: TTag, - children: T -): (ArrayToObj<T> & Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; -export function h<TTag extends string>(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>): Record<'root', TagToElement<TTag>>; -export function h<TTag extends string, TId extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>( - tag: TTag, - attributes: { $: TId } & Partial<ElementAttributes<TagToElement<TTag>>>, - children: T -): (ArrayToObj<T> & Record<TId, TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; +export function h<TTag extends string> + (tag: TTag): + TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function h<TTag extends string, T extends Children> + (tag: TTag, children: T): + (ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function h<TTag extends string> + (tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>): + TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function h<TTag extends string, T extends Children> + (tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: T): + (ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + export function h(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> { let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>; let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined; @@ -1800,25 +1825,29 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia children = args[1]; } - const match = SELECTOR_REGEX.exec(tag); + const match = H_REGEX.exec(tag); - if (!match) { + if (!match || !match.groups) { throw new Error('Bad use of h'); } - const tagName = match[1] || 'div'; + const tagName = match.groups['tag'] || 'div'; const el = document.createElement(tagName); - if (match[3]) { - el.id = match[3]; + if (match.groups['id']) { + el.id = match.groups['id']; } - if (match[4]) { - el.className = match[4].replace(/\./g, ' ').trim(); + if (match.groups['class']) { + el.className = match.groups['class'].replace(/\./g, ' ').trim(); } const result: Record<string, HTMLElement> = {}; + if (match.groups['name']) { + result[match.groups['name']] = el; + } + if (children) { for (const c of children) { if (c instanceof HTMLElement) { @@ -1833,10 +1862,6 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia } for (const [key, value] of Object.entries(attributes)) { - if (key === '$') { - result[value] = el; - continue; - } if (key === 'style') { for (const [cssKey, cssValue] of Object.entries(value)) { el.style.setProperty( diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 2e809e2ad48..e43d27ac623 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -9,8 +9,6 @@ import { DomEmitter } from 'vs/base/browser/event'; import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { raceCancellation } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { IMarkdownString, escapeDoubleQuotes, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent'; @@ -44,8 +42,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende const disposables = new DisposableStore(); let isDisposed = false; - const cts = disposables.add(new CancellationTokenSource()); - const element = createElement(options); const _uriMassage = function (part: string): string { @@ -96,11 +92,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende return uri.toString(); }; - // signal to code-block render that the - // element has been created - let signalInnerHTML: () => void; - const withInnerHTML = new Promise<void>(c => signalInnerHTML = c); - const renderer = new marked.Renderer(); renderer.image = (href: string, title: string, text: string) => { @@ -146,24 +137,14 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende return `<p>${text}</p>`; }; + // Will collect [id, renderedElement] tuples + const codeBlocks: Promise<[string, HTMLElement]>[] = []; + if (options.codeBlockRenderer) { renderer.code = (code, lang) => { - const value = options.codeBlockRenderer!(lang ?? '', code); - // when code-block rendering is async we return sync - // but update the node with the real result later. const id = defaultGenerator.nextId(); - raceCancellation(Promise.all([value, withInnerHTML]), cts.token).then(values => { - if (!isDisposed && values) { - const span = element.querySelector<HTMLDivElement>(`div[data-code="${id}"]`); - if (span) { - DOM.reset(span, values[0]); - } - options.asyncRenderCallback?.(); - } - }).catch(() => { - // ignore - }); - + const value = options.codeBlockRenderer!(lang ?? '', code); + codeBlocks.push(value.then(element => [id, element])); return `<div class="code" data-code="${id}">${escape(code)}</div>`; }; } @@ -277,8 +258,22 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende element.innerHTML = sanitizeRenderedMarkdown(markdown, markdownHtmlDoc.body.innerHTML) as unknown as string; - // signal that async code blocks can be now be inserted - signalInnerHTML!(); + if (codeBlocks.length > 0) { + Promise.all(codeBlocks).then((tuples) => { + if (isDisposed) { + return; + } + const renderedElements = new Map(tuples); + const placeholderElements = element.querySelectorAll<HTMLDivElement>(`div[data-code]`); + for (const placeholderElement of placeholderElements) { + const renderedElement = renderedElements.get(placeholderElement.dataset['code'] ?? ''); + if (renderedElement) { + DOM.reset(placeholderElement, renderedElement); + } + } + options.asyncRenderCallback?.(); + }); + } // signal size changes for image tags if (options.asyncRenderCallback) { @@ -294,7 +289,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende element, dispose: () => { isDisposed = true; - cts.cancel(); disposables.dispose(); } }; diff --git a/src/vs/base/browser/ui/button/button.css b/src/vs/base/browser/ui/button/button.css index f5c80d09184..4d612f9f674 100644 --- a/src/vs/base/browser/ui/button/button.css +++ b/src/vs/base/browser/ui/button/button.css @@ -49,7 +49,11 @@ .monaco-button-dropdown.disabled > .monaco-button.disabled, .monaco-button-dropdown.disabled > .monaco-button.disabled:focus, .monaco-button-dropdown.disabled > .monaco-button-dropdown-separator { - opacity: 0.4; + opacity: 0.4 !important; +} + +.monaco-button-dropdown > .monaco-button.monaco-text-button { + border-right-width: 0 !important; } .monaco-button-dropdown .monaco-button-dropdown-separator { @@ -62,6 +66,10 @@ width: 1px; } +.monaco-button-dropdown > .monaco-button.monaco-dropdown-button { + border-left-width: 0 !important; +} + .monaco-description-button { flex-direction: column; } diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 920160b9a15..855ba9e0c90 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -316,6 +316,16 @@ export class ButtonWithDropdown extends Disposable implements IButton { this.dropdownButton.style(styles); // Separator + const border = styles.buttonBorder ? styles.buttonBorder.toString() : ''; + + this.separatorContainer.style.borderTopWidth = border ? '1px' : ''; + this.separatorContainer.style.borderTopStyle = border ? 'solid' : ''; + this.separatorContainer.style.borderTopColor = border; + + this.separatorContainer.style.borderBottomWidth = border ? '1px' : ''; + this.separatorContainer.style.borderBottomStyle = border ? 'solid' : ''; + this.separatorContainer.style.borderBottomColor = border; + this.separatorContainer.style.backgroundColor = styles.buttonBackground?.toString() ?? ''; this.separator.style.backgroundColor = styles.buttonSeparator?.toString() ?? ''; } diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf Binary files differindex 9d4627dfb64..5abfa748fb5 100644 --- a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf +++ b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 7254351f697..f89a2f5d237 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -424,6 +424,7 @@ class TypeNavigationController<T> implements IDisposable { private list: List<T>, private view: ListView<T>, private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider<T>, + private keyboardNavigationEventFilter: IKeyboardNavigationEventFilter, private delegate: IKeyboardNavigationDelegate ) { this.updateOptions(list.options); @@ -448,12 +449,15 @@ class TypeNavigationController<T> implements IDisposable { return; } + let typing = false; + const onChar = this.enabledDisposables.add(Event.chain(this.enabledDisposables.add(new DomEmitter(this.view.domNode, 'keydown')).event)) .filter(e => !isInputElement(e.target as HTMLElement)) .filter(() => this.mode === TypeNavigationMode.Automatic || this.triggered) .map(event => new StandardKeyboardEvent(event)) + .filter(e => typing || this.keyboardNavigationEventFilter(e)) .filter(e => this.delegate.mightProducePrintableCharacter(e)) - .forEach(e => { e.preventDefault(); e.stopPropagation(); }) + .forEach(stopEvent) .map(event => event.browserEvent.key) .event; @@ -463,6 +467,9 @@ class TypeNavigationController<T> implements IDisposable { onInput(this.onInput, this, this.enabledDisposables); onClear(this.onClear, this, this.enabledDisposables); + onChar(() => typing = true, undefined, this.enabledDisposables); + onClear(() => typing = false, undefined, this.enabledDisposables); + this.enabled = true; this.triggered = false; } @@ -919,6 +926,10 @@ export class DefaultStyleController implements IStyleController { } } +export interface IKeyboardNavigationEventFilter { + (e: StandardKeyboardEvent): boolean; +} + export interface IListOptionsUpdate extends IListViewOptionsUpdate { readonly typeNavigationEnabled?: boolean; readonly typeNavigationMode?: TypeNavigationMode; @@ -934,6 +945,7 @@ export interface IListOptions<T> extends IListOptionsUpdate { readonly multipleSelectionController?: IMultipleSelectionController<T>; readonly styleController?: (suffix: string) => IStyleController; readonly accessibilityProvider?: IListAccessibilityProvider<T>; + readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter; // list view options readonly useShadows?: boolean; @@ -1373,7 +1385,7 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable { if (_options.keyboardNavigationLabelProvider) { const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate; - this.typeNavigationController = new TypeNavigationController(this, this.view, _options.keyboardNavigationLabelProvider, delegate); + this.typeNavigationController = new TypeNavigationController(this, this.view, _options.keyboardNavigationLabelProvider, _options.keyboardNavigationEventFilter ?? (() => true), delegate); this.disposables.add(this.typeNavigationController); } @@ -1785,6 +1797,10 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable { return this.view.domNode; } + getElementID(index: number): string { + return this.view.getElementDomId(index); + } + style(styles: IListStyles): void { this.styleController.style(styles); } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index ba4c6ec3af1..7a75aa8070a 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -684,10 +684,10 @@ export enum TreeFindMode { class FindWidget<T, TFilterData> extends Disposable { - private readonly elements = h('div.monaco-tree-type-filter', [ - h('div.monaco-tree-type-filter-grab.codicon.codicon-debug-gripper', { $: 'grab' }), - h('div.monaco-tree-type-filter-input', { $: 'findInput' }), - h('div.monaco-tree-type-filter-actionbar', { $: 'actionbar' }), + private readonly elements = h('.monaco-tree-type-filter', [ + h('.monaco-tree-type-filter-grab.codicon.codicon-debug-gripper@grab'), + h('.monaco-tree-type-filter-input@findInput'), + h('.monaco-tree-type-filter-actionbar@actionbar'), ]); set mode(mode: TreeFindMode) { @@ -1003,10 +1003,6 @@ function asTreeContextMenuEvent<T>(event: IListContextMenuEvent<ITreeNode<T, any }; } -export interface IKeyboardNavigationEventFilter { - (e: StandardKeyboardEvent): boolean; -} - export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions { readonly multipleSelectionSupport?: boolean; readonly typeNavigationEnabled?: boolean; @@ -1025,7 +1021,6 @@ export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTr readonly collapseByDefault?: boolean; // defaults to false readonly filter?: ITreeFilter<T, TFilterData>; readonly dnd?: ITreeDragAndDrop<T>; - readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter; readonly additionalScrollHeight?: number; } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 48cc343fc1b..a8b84679303 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -742,6 +742,16 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable return result; } + if (node !== this.root) { + const treeNode = this.tree.getNode(node); + + if (treeNode.collapsed) { + node.hasChildren = !!this.dataSource.hasChildren(node.element!); + node.stale = true; + return; + } + } + return this.doRefreshSubTree(node, recursive, viewStateContext); } diff --git a/src/vs/base/browser/ui/tree/media/tree.css b/src/vs/base/browser/ui/tree/media/tree.css index f44e796a572..f6dceb39e95 100644 --- a/src/vs/base/browser/ui/tree/media/tree.css +++ b/src/vs/base/browser/ui/tree/media/tree.css @@ -97,6 +97,10 @@ flex: 1; } +.monaco-tree-type-filter-input .monaco-inputbox { + height: 26px; +} + .monaco-tree-type-filter-input .monaco-inputbox > .ibwrapper > .input, .monaco-tree-type-filter-input .monaco-inputbox > .ibwrapper > .mirror { padding: 2px; diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index f9729a18770..cfaa9389600 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -15,8 +15,8 @@ export interface ITelemetryData { export type WorkbenchActionExecutedClassification = { owner: 'bpasero'; - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run.' }; + from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the component the action was run from.' }; }; export type WorkbenchActionExecutedEvent = { diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index e08e651d91e..da581de1cc6 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -560,6 +560,8 @@ export class Codicon implements CSSIcon { public static readonly bellSlash = new Codicon('bell-slash', { fontCharacter: '\\ec08' }); public static readonly bellSlashDot = new Codicon('bell-slash-dot', { fontCharacter: '\\ec09' }); public static readonly commentUnresolved = new Codicon('comment-unresolved', { fontCharacter: '\\ec0a' }); + public static readonly gitPullRequestGoToChanges = new Codicon('git-pull-request-go-to-changes', { fontCharacter: '\\ec0b' }); + public static readonly gitPullRequestNewChanges = new Codicon('git-pull-request-new-changes', { fontCharacter: '\\ec0c' }); // derived icons, that could become separate icons diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 4b153336f01..3710827d559 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -715,22 +715,28 @@ export class TernarySearchTree<K, V> { yield* this._entries(this._root); } - private *_entries(node: TernarySearchTreeNode<K, V> | undefined): IterableIterator<[K, V]> { + private _entries(node: TernarySearchTreeNode<K, V> | undefined): IterableIterator<[K, V]> { + const result: [K, V][] = []; + this._dfsEntries(node, result); + return result[Symbol.iterator](); + } + + private _dfsEntries(node: TernarySearchTreeNode<K, V> | undefined, bucket: [K, V][]) { // DFS if (!node) { return; } if (node.left) { - yield* this._entries(node.left); + this._dfsEntries(node.left, bucket); } if (node.value) { - yield [node.key!, node.value]; + bucket.push([node.key!, node.value]); } if (node.mid) { - yield* this._entries(node.mid); + this._dfsEntries(node.mid, bucket); } if (node.right) { - yield* this._entries(node.right); + this._dfsEntries(node.right, bucket); } } diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index 3e1cb2a7354..e3844cd39e8 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -275,3 +275,9 @@ export type UriDto<T> = { [K in keyof T]: T[K] extends URI export function assertNever(value: never, message = 'Unreachable'): never { throw new Error(message); } + +/** + * Given an object with all optional properties, requires at least one to be defined. + * i.e. AtLeastOne<MyObject>; + */ +export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]; diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 7d9b29a6e3a..d89c22a4f0d 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -36,6 +36,10 @@ export interface ISandboxNodeProcess extends INodeProcess { /** * The `process.pid` property returns the PID of the process. + * + * @deprecated this property will be removed once sandbox is enabled. + * + * TODO@bpasero remove this property when sandbox is on */ readonly pid: number; diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index 435f0066b9c..1ad53561927 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as dom from 'vs/base/browser/dom'; -const $ = dom.$; +import { $, h, multibyteAwareBtoa } from 'vs/base/browser/dom'; suite('dom', () => { test('hasClass', () => { @@ -73,9 +72,9 @@ suite('dom', () => { }); test('multibyteAwareBtoa', () => { - assert.ok(dom.multibyteAwareBtoa('hello world').length > 0); - assert.ok(dom.multibyteAwareBtoa('平仮名').length > 0); - assert.ok(dom.multibyteAwareBtoa(new Array(100000).fill('vs').join('')).length > 0); // https://github.com/microsoft/vscode/issues/112013 + assert.ok(multibyteAwareBtoa('hello world').length > 0); + assert.ok(multibyteAwareBtoa('平仮名').length > 0); + assert.ok(multibyteAwareBtoa(new Array(100000).fill('vs').join('')).length > 0); // https://github.com/microsoft/vscode/issues/112013 }); suite('$', () => { @@ -129,4 +128,152 @@ suite('dom', () => { assert.strictEqual(firstChild.textContent, 'foobar'); }); }); + + suite('h', () => { + test('should build simple nodes', () => { + const div = h('div'); + assert(div.root instanceof HTMLElement); + assert.strictEqual(div.root.tagName, 'DIV'); + + const span = h('span'); + assert(span.root instanceof HTMLElement); + assert.strictEqual(span.root.tagName, 'SPAN'); + + const img = h('img'); + assert(img.root instanceof HTMLElement); + assert.strictEqual(img.root.tagName, 'IMG'); + }); + + test('should handle ids and classes', () => { + const divId = h('div#myid'); + assert.strictEqual(divId.root.tagName, 'DIV'); + assert.strictEqual(divId.root.id, 'myid'); + + const divClass = h('div.a'); + assert.strictEqual(divClass.root.tagName, 'DIV'); + assert.strictEqual(divClass.root.classList.length, 1); + assert(divClass.root.classList.contains('a')); + + const divClasses = h('div.a.b.c'); + assert.strictEqual(divClasses.root.tagName, 'DIV'); + assert.strictEqual(divClasses.root.classList.length, 3); + assert(divClasses.root.classList.contains('a')); + assert(divClasses.root.classList.contains('b')); + assert(divClasses.root.classList.contains('c')); + + const divAll = h('div#myid.a.b.c'); + assert.strictEqual(divAll.root.tagName, 'DIV'); + assert.strictEqual(divAll.root.id, 'myid'); + assert.strictEqual(divAll.root.classList.length, 3); + assert(divAll.root.classList.contains('a')); + assert(divAll.root.classList.contains('b')); + assert(divAll.root.classList.contains('c')); + + const spanId = h('span#myid'); + assert.strictEqual(spanId.root.tagName, 'SPAN'); + assert.strictEqual(spanId.root.id, 'myid'); + + const spanClass = h('span.a'); + assert.strictEqual(spanClass.root.tagName, 'SPAN'); + assert.strictEqual(spanClass.root.classList.length, 1); + assert(spanClass.root.classList.contains('a')); + + const spanClasses = h('span.a.b.c'); + assert.strictEqual(spanClasses.root.tagName, 'SPAN'); + assert.strictEqual(spanClasses.root.classList.length, 3); + assert(spanClasses.root.classList.contains('a')); + assert(spanClasses.root.classList.contains('b')); + assert(spanClasses.root.classList.contains('c')); + + const spanAll = h('span#myid.a.b.c'); + assert.strictEqual(spanAll.root.tagName, 'SPAN'); + assert.strictEqual(spanAll.root.id, 'myid'); + assert.strictEqual(spanAll.root.classList.length, 3); + assert(spanAll.root.classList.contains('a')); + assert(spanAll.root.classList.contains('b')); + assert(spanAll.root.classList.contains('c')); + }); + + test('should implicitly handle ids and classes', () => { + const divId = h('#myid'); + assert.strictEqual(divId.root.tagName, 'DIV'); + assert.strictEqual(divId.root.id, 'myid'); + + const divClass = h('.a'); + assert.strictEqual(divClass.root.tagName, 'DIV'); + assert.strictEqual(divClass.root.classList.length, 1); + assert(divClass.root.classList.contains('a')); + + const divClasses = h('.a.b.c'); + assert.strictEqual(divClasses.root.tagName, 'DIV'); + assert.strictEqual(divClasses.root.classList.length, 3); + assert(divClasses.root.classList.contains('a')); + assert(divClasses.root.classList.contains('b')); + assert(divClasses.root.classList.contains('c')); + + const divAll = h('#myid.a.b.c'); + assert.strictEqual(divAll.root.tagName, 'DIV'); + assert.strictEqual(divAll.root.id, 'myid'); + assert.strictEqual(divAll.root.classList.length, 3); + assert(divAll.root.classList.contains('a')); + assert(divAll.root.classList.contains('b')); + assert(divAll.root.classList.contains('c')); + }); + + test('should handle @ identifiers', () => { + const implicit = h('@el'); + assert.strictEqual(implicit.root, implicit.el); + assert.strictEqual(implicit.el.tagName, 'DIV'); + + const explicit = h('div@el'); + assert.strictEqual(explicit.root, explicit.el); + assert.strictEqual(explicit.el.tagName, 'DIV'); + + const implicitId = h('#myid@el'); + assert.strictEqual(implicitId.root, implicitId.el); + assert.strictEqual(implicitId.el.tagName, 'DIV'); + assert.strictEqual(implicitId.root.id, 'myid'); + + const explicitId = h('div#myid@el'); + assert.strictEqual(explicitId.root, explicitId.el); + assert.strictEqual(explicitId.el.tagName, 'DIV'); + assert.strictEqual(explicitId.root.id, 'myid'); + + const implicitClass = h('.a@el'); + assert.strictEqual(implicitClass.root, implicitClass.el); + assert.strictEqual(implicitClass.el.tagName, 'DIV'); + assert.strictEqual(implicitClass.root.classList.length, 1); + assert(implicitClass.root.classList.contains('a')); + + const explicitClass = h('div.a@el'); + assert.strictEqual(explicitClass.root, explicitClass.el); + assert.strictEqual(explicitClass.el.tagName, 'DIV'); + assert.strictEqual(explicitClass.root.classList.length, 1); + assert(explicitClass.root.classList.contains('a')); + }); + }); + + test('should recurse', () => { + const result = h('div.code-view', [ + h('div.title@title'), + h('div.container', [ + h('div.gutter@gutterDiv'), + h('span@editor'), + ]), + ]); + + assert.strictEqual(result.root.tagName, 'DIV'); + assert.strictEqual(result.root.className, 'code-view'); + assert.strictEqual(result.root.childElementCount, 2); + assert.strictEqual(result.root.firstElementChild, result.title); + assert.strictEqual(result.title.tagName, 'DIV'); + assert.strictEqual(result.title.className, 'title'); + assert.strictEqual(result.title.childElementCount, 0); + assert.strictEqual(result.gutterDiv.tagName, 'DIV'); + assert.strictEqual(result.gutterDiv.className, 'gutter'); + assert.strictEqual(result.gutterDiv.childElementCount, 0); + assert.strictEqual(result.editor.tagName, 'SPAN'); + assert.strictEqual(result.editor.className, ''); + assert.strictEqual(result.editor.childElementCount, 0); + }); }); diff --git a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts index 2fc37090431..d10d76b0b85 100644 --- a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts @@ -8,6 +8,7 @@ import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { timeout } from 'vs/base/common/async'; +import { Iterable } from 'vs/base/common/iterator'; interface Element { id: string; @@ -435,4 +436,60 @@ suite('AsyncDataTree', function () { assert.deepStrictEqual(Array.from(container.querySelectorAll('.monaco-list-row')).map(e => e.textContent), ['a', 'b2']); }); + + test('issue #121567', async () => { + const container = document.createElement('div'); + + const calls: Element[] = []; + const dataSource = new class implements IAsyncDataSource<Element, Element> { + hasChildren(element: Element): boolean { + return !!element.children && element.children.length > 0; + } + async getChildren(element: Element) { + calls.push(element); + return element.children ?? Iterable.empty(); + } + }; + + const model = new Model({ + id: 'root', + children: [{ + id: 'a', children: [{ + id: 'aa' + }] + }] + }); + const a = model.get('a'); + + const tree = new AsyncDataTree<Element, Element>('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() }); + tree.layout(200); + + await tree.setInput(model.root); + assert.strictEqual(calls.length, 1, 'There should be a single getChildren call for the root'); + assert(tree.isCollapsible(a), 'a is collapsible'); + assert(tree.isCollapsed(a), 'a is collapsed'); + + await tree.updateChildren(a, false); + assert.strictEqual(calls.length, 1, 'There should be no changes to the calls list, since a was collapsed'); + assert(tree.isCollapsible(a), 'a is collapsible'); + assert(tree.isCollapsed(a), 'a is collapsed'); + + const children = a.children; + a.children = []; + await tree.updateChildren(a, false); + assert.strictEqual(calls.length, 1, 'There should still be no changes to the calls list, since a was collapsed'); + assert(!tree.isCollapsible(a), 'a is no longer collapsible'); + assert(tree.isCollapsed(a), 'a is collapsed'); + + a.children = children; + await tree.updateChildren(a, false); + assert.strictEqual(calls.length, 1, 'There should still be no changes to the calls list, since a was collapsed'); + assert(tree.isCollapsible(a), 'a is collapsible again'); + assert(tree.isCollapsed(a), 'a is collapsed'); + + await tree.expand(a); + assert.strictEqual(calls.length, 2, 'Finally, there should be a getChildren call for a'); + assert(tree.isCollapsible(a), 'a is still collapsible'); + assert(!tree.isCollapsed(a), 'a is expanded'); + }); }); diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 4c15c3ce143..0f97f800ec6 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -21,18 +21,13 @@ flakySuite('PFS', function () { let testDir: string; setup(() => { - configureFlushOnWrite(true); // but enable flushing for the purpose of these tests testDir = getRandomTestPath(tmpdir(), 'vsctests', 'pfs'); return Promises.mkdir(testDir, { recursive: true }); }); - teardown(async () => { - try { - await Promises.rm(testDir); - } finally { - configureFlushOnWrite(false); - } + teardown(() => { + return Promises.rm(testDir); }); test('writeFile', async () => { @@ -375,24 +370,36 @@ flakySuite('PFS', function () { const smallData = 'Hello World'; const bigData = (new Array(100 * 1024)).join('Large String\n'); - return testWriteFileAndFlush(smallData, smallData, bigData, bigData); + return testWriteFile(smallData, smallData, bigData, bigData); + }); + + test('writeFile (string) - flush on write', async () => { + configureFlushOnWrite(true); + try { + const smallData = 'Hello World'; + const bigData = (new Array(100 * 1024)).join('Large String\n'); + + return await testWriteFile(smallData, smallData, bigData, bigData); + } finally { + configureFlushOnWrite(false); + } }); test('writeFile (Buffer)', async () => { const smallData = 'Hello World'; const bigData = (new Array(100 * 1024)).join('Large String\n'); - return testWriteFileAndFlush(Buffer.from(smallData), smallData, Buffer.from(bigData), bigData); + return testWriteFile(Buffer.from(smallData), smallData, Buffer.from(bigData), bigData); }); test('writeFile (UInt8Array)', async () => { const smallData = 'Hello World'; const bigData = (new Array(100 * 1024)).join('Large String\n'); - return testWriteFileAndFlush(VSBuffer.fromString(smallData).buffer, smallData, VSBuffer.fromString(bigData).buffer, bigData); + return testWriteFile(VSBuffer.fromString(smallData).buffer, smallData, VSBuffer.fromString(bigData).buffer, bigData); }); - async function testWriteFileAndFlush( + async function testWriteFile( smallData: string | Buffer | Uint8Array, smallDataValue: string, bigData: string | Buffer | Uint8Array, diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts index 0116d6e8aa8..3de8633f588 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts @@ -59,7 +59,7 @@ class ProfileExtensionsCleaner extends Disposable { this.onDidChangeProfiles({ added: this.userDataProfilesService.profiles, removed: [], all: this.userDataProfilesService.profiles }); } - private async onDidChangeProfiles({ added, removed, all }: DidChangeProfilesEvent): Promise<void> { + private async onDidChangeProfiles({ added, removed, all }: Omit<DidChangeProfilesEvent, 'updated'>): Promise<void> { try { await Promise.all(removed.map(profile => profile.extensionsResource ? this.removeExtensionsFromProfile(profile.extensionsResource) : Promise.resolve())); } catch (error) { diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 1da1db197b9..670685ffd9d 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -1181,7 +1181,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/code/node/cli.ts b/src/vs/code/node/cli.ts index 735a7e39669..c64e6fe0503 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -62,20 +62,20 @@ export async function main(argv: string[]): Promise<any> { } // Shell integration - else if (args['shell-integration']) { + else if (args['locate-shell-integration-path']) { // Silently fail when the terminal is not VS Code's integrated terminal if (process.env['TERM_PROGRAM'] !== 'vscode') { return; } let file: string; - switch (args['shell-integration']) { - // Usage: `[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --shell-integration bash)"` + switch (args['locate-shell-integration-path']) { + // Usage: `[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path bash)"` case 'bash': file = 'shellIntegration-bash.sh'; break; - // Usage: `if ($env:TERM_PROGRAM -eq "vscode") { . "$(code --shell-integration pwsh)" }` + // Usage: `if ($env:TERM_PROGRAM -eq "vscode") { . "$(code --locate-shell-integration-path pwsh)" }` case 'pwsh': file = 'shellIntegration.ps1'; break; - // Usage: `[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --shell-integration zsh)"` + // Usage: `[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path zsh)"` case 'zsh': file = 'shellIntegration-rc.zsh'; break; - default: throw new Error('Error using --shell-integration: Invalid shell type'); + default: throw new Error('Error using --locate-shell-integration-path: Invalid shell type'); } console.log(join(dirname(FileAccess.asFileUri('', require)).fsPath, 'out', 'vs', 'workbench', 'contrib', 'terminal', 'browser', 'media', file)); } diff --git a/src/vs/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index 93b20f1d2ed..f61142282d3 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -339,8 +339,9 @@ export abstract class EditorAction extends EditorCommand { protected reportTelemetry(accessor: ServicesAccessor, editor: ICodeEditor) { type EditorActionInvokedClassification = { owner: 'alexdima'; - name: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + comment: 'An editor action has been invoked.'; + name: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the action that was invoked.' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was invoked.' }; }; type EditorActionInvokedEvent = { name: string; diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 56870b5170c..8b751e86828 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -163,7 +163,7 @@ export class OpenerService implements IOpenerService { // validate against the original URI that this URI resolves to, if one exists const validationTarget = this._resolvedUriTargets.get(targetURI) ?? target; for (const validator of this._validators) { - if (!(await validator.shouldOpen(validationTarget))) { + if (!(await validator.shouldOpen(validationTarget, options))) { return false; } } diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts index 3f5d8862f26..0ac2afa5fa3 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts +++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts @@ -27,6 +27,7 @@ export class LineNumbersOverlay extends DynamicViewOverlay { private _lineNumbersLeft!: number; private _lineNumbersWidth!: number; private _lastCursorModelPosition: Position; + private _lastCursorViewPosition: Position; private _renderResult: string[] | null; private _activeLineNumber: number; @@ -37,6 +38,7 @@ export class LineNumbersOverlay extends DynamicViewOverlay { this._readConfig(); this._lastCursorModelPosition = new Position(1, 1); + this._lastCursorViewPosition = new Position(1, 1); this._renderResult = null; this._activeLineNumber = 1; this._context.addEventHandler(this); @@ -68,6 +70,7 @@ export class LineNumbersOverlay extends DynamicViewOverlay { } public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { const primaryViewPosition = e.selections[0].getPosition(); + this._lastCursorViewPosition = primaryViewPosition; this._lastCursorModelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(primaryViewPosition); let shouldRender = false; @@ -112,14 +115,6 @@ export class LineNumbersOverlay extends DynamicViewOverlay { return this._renderCustomLineNumbers(modelLineNumber); } - if (this._renderLineNumbers === RenderLineNumbersType.Relative) { - const diff = Math.abs(this._lastCursorModelPosition.lineNumber - modelLineNumber); - if (diff === 0) { - return '<span class="relative-current-line-number">' + modelLineNumber + '</span>'; - } - return String(diff); - } - if (this._renderLineNumbers === RenderLineNumbersType.Interval) { if (this._lastCursorModelPosition.lineNumber === modelLineNumber) { return String(modelLineNumber); @@ -144,6 +139,45 @@ export class LineNumbersOverlay extends DynamicViewOverlay { const visibleEndLineNumber = ctx.visibleRange.endLineNumber; const common = '<div class="' + LineNumbersOverlay.CLASS_NAME + lineHeightClassName + '" style="left:' + this._lineNumbersLeft + 'px;width:' + this._lineNumbersWidth + 'px;">'; + let relativeLineNumbers: number[] | null = null; + if (this._renderLineNumbers === RenderLineNumbersType.Relative) { + relativeLineNumbers = new Array(visibleEndLineNumber - visibleStartLineNumber + 1); + + if (this._lastCursorViewPosition.lineNumber >= visibleStartLineNumber && this._lastCursorViewPosition.lineNumber <= visibleEndLineNumber) { + relativeLineNumbers[this._lastCursorViewPosition.lineNumber - visibleStartLineNumber] = this._lastCursorModelPosition.lineNumber; + } + + // Iterate up to compute relative line numbers + { + let value = 0; + for (let lineNumber = this._lastCursorViewPosition.lineNumber + 1; lineNumber <= visibleEndLineNumber; lineNumber++) { + const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber, 1)); + const isWrappedLine = (modelPosition.column !== 1); + if (!isWrappedLine) { + value++; + } + if (lineNumber >= visibleStartLineNumber) { + relativeLineNumbers[lineNumber - visibleStartLineNumber] = isWrappedLine ? 0 : value; + } + } + } + + // Iterate down to compute relative line numbers + { + let value = 0; + for (let lineNumber = this._lastCursorViewPosition.lineNumber - 1; lineNumber >= visibleStartLineNumber; lineNumber--) { + const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber, 1)); + const isWrappedLine = (modelPosition.column !== 1); + if (!isWrappedLine) { + value++; + } + if (lineNumber <= visibleEndLineNumber) { + relativeLineNumbers[lineNumber - visibleStartLineNumber] = isWrappedLine ? 0 : value; + } + } + } + } + const lineCount = this._context.viewModel.getLineCount(); const output: string[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { @@ -157,7 +191,20 @@ export class LineNumbersOverlay extends DynamicViewOverlay { } } - const renderLineNumber = this._getLineRenderLineNumber(lineNumber); + let renderLineNumber: string; + if (relativeLineNumbers) { + const relativeLineNumber = relativeLineNumbers[lineIndex]; + if (this._lastCursorViewPosition.lineNumber === lineNumber) { + // current line! + renderLineNumber = `<span class="relative-current-line-number">${relativeLineNumber}</span>`; + } else if (relativeLineNumber) { + renderLineNumber = String(relativeLineNumber); + } else { + renderLineNumber = ''; + } + } else { + renderLineNumber = this._getLineRenderLineNumber(lineNumber); + } if (renderLineNumber) { if (lineNumber === this._activeLineNumber) { diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index f4e8c0d10db..0cc2cfa02af 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -1177,9 +1177,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData || text.length === 0) { return; } - const startPosition = this._modelData.viewModel.getSelection().getStartPosition(); - this._modelData.viewModel.paste(text, pasteOnNewLine, multicursorText, source); - const endPosition = this._modelData.viewModel.getSelection().getStartPosition(); + const viewModel = this._modelData.viewModel; + const startPosition = viewModel.getSelection().getStartPosition(); + viewModel.paste(text, pasteOnNewLine, multicursorText, source); + const endPosition = viewModel.getSelection().getStartPosition(); if (source === 'keyboard') { this._onDidPaste.fire({ range: new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column), diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts index d4485d47cc4..9ea7e35847c 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts @@ -223,6 +223,10 @@ function collectBrackets( level: number, levelPerBracketType: Map<string, number> ): void { + if (level > 200) { + return; + } + if (node.kind === AstNodeKind.List) { for (const child of node.children) { nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); @@ -333,6 +337,10 @@ function collectBracketPairs( level: number, levelPerBracketType: Map<string, number> ) { + if (level > 200) { + return; + } + if (node.kind === AstNodeKind.Pair) { let levelPerBracket = 0; if (levelPerBracketType) { diff --git a/src/vs/editor/common/model/textModelSearch.ts b/src/vs/editor/common/model/textModelSearch.ts index 0d58500a2e6..87c4bd8ecf7 100644 --- a/src/vs/editor/common/model/textModelSearch.ts +++ b/src/vs/editor/common/model/textModelSearch.ts @@ -74,6 +74,10 @@ export function isMultilineRegexSource(searchString: string): boolean { for (let i = 0, len = searchString.length; i < len; i++) { const chCode = searchString.charCodeAt(i); + if (chCode === CharCode.LineFeed) { + return true; + } + if (chCode === CharCode.Backslash) { // move to next char diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index b504fc14366..120da61f3f5 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -472,12 +472,12 @@ export class TextModelTokenization extends Disposable { } if (newNonWhitespaceIndex < nonWhitespaceColumn) { + fakeLines.push(this._textModel.getLineContent(i)); + nonWhitespaceColumn = newNonWhitespaceIndex; initialState = this._tokenizationStateStore.getBeginState(i - 1); if (initialState) { break; } - fakeLines.push(this._textModel.getLineContent(i)); - nonWhitespaceColumn = newNonWhitespaceIndex; } } diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts index 408079626e5..62647863cf5 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts @@ -11,7 +11,7 @@ import { Lazy } from 'vs/base/common/lazy'; import { Disposable } from 'vs/base/common/lifecycle'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, EditorCommand, registerEditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { IPosition } from 'vs/editor/common/core/position'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; @@ -32,6 +32,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CodeActionModel, CodeActionsState, SUPPORTED_CODE_ACTIONS } from './codeActionModel'; import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionFilter, CodeActionKind, CodeActionTrigger, CodeActionTriggerSource } from './types'; +import { Context } from 'vs/editor/contrib/codeAction/browser/codeActionMenu'; function contextKeyForSupportedActions(kind: CodeActionKind) { return ContextKeyExpr.regex( @@ -132,6 +133,24 @@ export class QuickFixController extends Disposable implements IEditorContributio this._ui.getValue().update(newState); } + public hideCodeActionMenu() { + if (this._ui.hasValue()) { + this._ui.getValue().hideCodeActionWidget(); + } + } + + public navigateCodeActionList(navUp: Boolean) { + if (this._ui.hasValue()) { + this._ui.getValue().navigateList(navUp); + } + } + + public selectedOption() { + if (this._ui.hasValue()) { + this._ui.getValue().onEnter(); + } + } + public showCodeActions(trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition) { return this._ui.getValue().showCodeActionList(trigger, actions, at, { includeDisabledActions: false, fromLightbulb: false }); } @@ -490,3 +509,61 @@ export class AutoFixAction extends EditorAction { CodeActionAutoApply.IfSingle, undefined, CodeActionTriggerSource.AutoFix); } } + +const CodeActionContribution = EditorCommand.bindToContribution<QuickFixController>(QuickFixController.get); + +const weight = KeybindingWeight.EditorContrib + 90; + +registerEditorCommand(new CodeActionContribution({ + id: 'hideCodeActionMenuWidget', + precondition: Context.Visible, + handler(x) { + x.hideCodeActionMenu(); + }, + kbOpts: { + weight: weight, + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape] + } +})); + +registerEditorCommand(new CodeActionContribution({ + id: 'focusPreviousCodeAction', + precondition: Context.Visible, + handler(x) { + x.navigateCodeActionList(true); + }, + kbOpts: { + weight: weight + 100000, + primary: KeyCode.UpArrow, + secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow], + } +})); + +registerEditorCommand(new CodeActionContribution({ + id: 'focusNextCodeAction', + precondition: Context.Visible, + handler(x) { + x.navigateCodeActionList(false); + }, + kbOpts: { + weight: weight + 100000, + primary: KeyCode.DownArrow, + secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow], + } +})); + +registerEditorCommand(new CodeActionContribution({ + id: 'onEnterSelectCodeAction', + precondition: Context.Visible, + handler(x) { + x.selectedOption(); + }, + kbOpts: { + weight: weight + 100000, + primary: KeyCode.Enter, + secondary: [KeyMod.Shift | KeyCode.Tab], + } +})); + + diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts b/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts index 359bbf7941f..59cd3327bea 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts @@ -5,7 +5,7 @@ import { registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { AutoFixAction, CodeActionCommand, FixAllAction, OrganizeImportsAction, QuickFixAction, QuickFixController, RefactorAction, RefactorPreview, SourceAction } from 'vs/editor/contrib/codeAction/browser/codeActionCommands'; - +import 'vs/editor/contrib/codeAction/browser/codeActionWidgetContribution'; registerEditorContribution(QuickFixController.ID, QuickFixController); registerEditorAction(QuickFixAction); diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts index 5536c1c1a45..a2d66113907 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts @@ -3,25 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getDomNodePagePosition } from 'vs/base/browser/dom'; +import * as dom from 'vs/base/browser/dom'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; +import { IListEvent, IListRenderer } from 'vs/base/browser/ui/list/list'; +import { List } from 'vs/base/browser/ui/list/listWidget'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { canceled } from 'vs/base/common/errors'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, MutableDisposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import 'vs/css!./media/action'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; -import { ScrollType } from 'vs/editor/common/editorCommon'; +import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { CodeAction, Command } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { codeActionCommandId, CodeActionItem, CodeActionSet, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/browser/types'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export const Context = { + Visible: new RawContextKey<boolean>('CodeActionMenuVisible', false, localize('CodeActionMenuVisible', "Whether the code action list widget is visible")) +}; interface CodeActionWidgetDelegate { onSelectCodeAction: (action: CodeActionItem, trigger: CodeActionTrigger) => Promise<any>; @@ -50,13 +62,99 @@ export interface CodeActionShowOptions { readonly includeDisabledActions: boolean; readonly fromLightbulb?: boolean; } +export interface ICodeActionMenuItem { + title: string; + detail: string; + action: IAction; + decoratorRight?: string; + isSeparator?: boolean; + isEnabled: boolean; + index: number; + disposables?: IDisposable[]; +} -export class CodeActionMenu extends Disposable { +export interface ICodeMenuOptions { + useCustomDrawn?: boolean; + ariaLabel?: string; + ariaDescription?: string; + minBottomMargin?: number; + optionsAsChildren?: boolean; +} - private _visible: boolean = false; +export interface ICodeActionMenuTemplateData { + root: HTMLElement; + text: HTMLElement; + detail: HTMLElement; + decoratorRight: HTMLElement; + disposables: IDisposable[]; +} + +const TEMPLATE_ID = 'codeActionWidget'; +const codeActionLineHeight = 26; + +class CodeMenuRenderer implements IListRenderer<ICodeActionMenuItem, ICodeActionMenuTemplateData> { + get templateId(): string { return TEMPLATE_ID; } + + renderTemplate(container: HTMLElement): ICodeActionMenuTemplateData { + const data: ICodeActionMenuTemplateData = Object.create(null); + data.disposables = []; + data.root = container; + data.text = document.createElement('span'); + // data.detail = document.createElement(''); + container.append(data.text); + // container.append(data.detail); + + return data; + } + renderElement(element: ICodeActionMenuItem, index: number, templateData: ICodeActionMenuTemplateData): void { + const data: ICodeActionMenuTemplateData = templateData; + + const text = element.title; + // const detail = element.detail; + + const isEnabled = element.isEnabled; + const isSeparator = element.isSeparator; + + data.text.textContent = text; + // data.detail.textContent = detail; + + if (!isEnabled) { + data.root.classList.add('option-disabled'); + data.root.style.backgroundColor = 'transparent !important'; + } else { + data.root.classList.remove('option-disabled'); + } + + if (isSeparator) { + data.root.classList.add('separator'); + data.root.style.height = '10px'; + } + + } + disposeTemplate(templateData: ICodeActionMenuTemplateData): void { + templateData.disposables = dispose(templateData.disposables); + } +} + +export class CodeActionMenu extends Disposable implements IEditorContribution { private readonly _showingActions = this._register(new MutableDisposable<CodeActionSet>()); + private codeActionList = this._register(new MutableDisposable<List<ICodeActionMenuItem>>()); + private options: ICodeActionMenuItem[] = []; + private _visible: boolean = false; + private _ctxMenuWidgetVisible: IContextKey<boolean>; + private viewItems: ICodeActionMenuItem[] = []; + private focusedEnabledItem: number | undefined; + private currSelectedItem: number = 0; + private hasSeperator: boolean = false; + + public static readonly ID: string = 'editor.contrib.codeActionMenu'; + + public static get(editor: ICodeEditor): CodeActionMenu | null { + return editor.getContribution<CodeActionMenu>(CodeActionMenu.ID); + } private readonly _keybindingResolver: CodeActionKeybindingResolver; + private listRenderer: CodeMenuRenderer = new CodeMenuRenderer(); constructor( private readonly _editor: ICodeEditor, @@ -64,20 +162,222 @@ export class CodeActionMenu extends Disposable { @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IThemeService _themeService: IThemeService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IContextViewService private readonly _contextViewService: IContextViewService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, ) { super(); this._keybindingResolver = new CodeActionKeybindingResolver({ getKeybindings: () => keybindingService.getKeybindings() }); + + this._ctxMenuWidgetVisible = Context.Visible.bindTo(this._contextKeyService); } get isVisible(): boolean { return this._visible; } + private isCodeActionWidgetEnabled(model: ITextModel): boolean { + return this._configurationService.getValue('editor.experimental.useCustomCodeActionMenu', { + resource: model.uri + }); + } + + private _onListSelection(e: IListEvent<ICodeActionMenuItem>): void { + if (e.elements.length) { + e.elements.forEach(element => { + if (element.isEnabled) { + element.action.run(); + } + }); + this.hideCodeActionWidget(); + } + } + + + private renderCodeActionMenuList(element: HTMLElement, inputArray: IAction[]): IDisposable { + const renderDisposables = new DisposableStore(); + const renderMenu = document.createElement('div'); + + renderMenu.id = 'codeActionMenuWidget'; + renderMenu.classList.add('codeActionMenuWidget'); + + element.appendChild(renderMenu); + + this.codeActionList.value = new List('codeActionWidget', renderMenu, { + getHeight(element) { + if (element.isSeparator) { + return 10; + } + return codeActionLineHeight; + }, + getTemplateId(element) { + return 'codeActionWidget'; + } + }, [this.listRenderer], { keyboardSupport: false } + ); + + renderDisposables.add(this.codeActionList.value.onDidChangeSelection(e => this._onListSelection(e))); + renderDisposables.add(this._editor.onDidLayoutChange(e => this.hideCodeActionWidget())); + + + // Populating the list widget and tracking enabled options. + inputArray.forEach((item, index) => { + const currIsSeparator = item.class === 'separator'; + if (currIsSeparator) { + // set to true forever + this.hasSeperator = true; + } + const menuItem = <ICodeActionMenuItem>{ title: item.label, detail: item.tooltip, action: inputArray[index], isEnabled: item.enabled, isSeparator: currIsSeparator, index }; + if (item.enabled) { + this.viewItems.push(menuItem); + } + this.options.push(menuItem); + }); + + this.codeActionList.value.splice(0, this.codeActionList.value.length, this.options); + + const height = this.hasSeperator ? (inputArray.length - 1) * codeActionLineHeight + 10 : inputArray.length * codeActionLineHeight; + renderMenu.style.height = String(height) + 'px'; + this.codeActionList.value.layout(height); + + // For finding width dynamically (not using resize observer) + const arr: number[] = []; + this.options.forEach((item, index) => { + if (!this.codeActionList.value) { + return; + } + const element = document.getElementById(this.codeActionList.value?.getElementID(index))?.getElementsByTagName('span')[0].offsetWidth; + arr.push(Number(element)); + }); + + // resize observer - can be used in the future since list widget supports dynamic height but not width + const maxWidth = Math.max(...arr); + + // 40 is the additional padding for the list widget (20 left, 20 right) + renderMenu.style.width = maxWidth + 52 + 'px'; + this.codeActionList.value?.layout(height, maxWidth); + + // List selection + this.focusedEnabledItem = 0; + this.currSelectedItem = this.viewItems[0].index; + this.codeActionList.value.setFocus([this.currSelectedItem]); + + // List Focus + this.codeActionList.value.domFocus(); + const focusTracker = dom.trackFocus(element); + const blurListener = focusTracker.onDidBlur(() => { + this.hideCodeActionWidget(); + // this._contextViewService.hideContextView({ source: this }); + }); + renderDisposables.add(blurListener); + renderDisposables.add(focusTracker); + this._ctxMenuWidgetVisible.set(true); + + return renderDisposables; + } + + protected focusPrevious() { + if (typeof this.focusedEnabledItem === 'undefined') { + this.focusedEnabledItem = this.viewItems[0].index; + } else if (this.viewItems.length <= 1) { + return false; + } + + const startIndex = this.focusedEnabledItem; + let item: ICodeActionMenuItem; + + do { + this.focusedEnabledItem = this.focusedEnabledItem - 1; + if (this.focusedEnabledItem < 0) { + this.focusedEnabledItem = this.viewItems.length - 1; + } + item = this.viewItems[this.focusedEnabledItem]; + this.codeActionList.value?.setFocus([item.index]); + this.currSelectedItem = item.index; + } while (this.focusedEnabledItem !== startIndex && ((!item.isEnabled) || item.action.id === Separator.ID)); + + return true; + } + + protected focusNext() { + if (typeof this.focusedEnabledItem === 'undefined') { + this.focusedEnabledItem = this.viewItems.length - 1; + } else if (this.viewItems.length <= 1) { + return false; + } + + const startIndex = this.focusedEnabledItem; + let item: ICodeActionMenuItem; + + do { + this.focusedEnabledItem = (this.focusedEnabledItem + 1) % this.viewItems.length; + item = this.viewItems[this.focusedEnabledItem]; + this.codeActionList.value?.setFocus([item.index]); + this.currSelectedItem = item.index; + } while (this.focusedEnabledItem !== startIndex && ((!item.isEnabled) || item.action.id === Separator.ID)); + + return true; + } + + public navigateListWithKeysUp() { + this.focusPrevious(); + } + + public navigateListWithKeysDown() { + this.focusNext(); + } + + public onEnterSet() { + this.codeActionList.value?.setSelection([this.currSelectedItem]); + } + + override dispose() { + super.dispose(); + } + + hideCodeActionWidget() { + this._ctxMenuWidgetVisible.reset(); + this.options = []; + this.viewItems = []; + this.focusedEnabledItem = 0; + this.currSelectedItem = 0; + this.hasSeperator = false; + this._contextViewService.hideContextView({ source: this }); + } + + codeActionTelemetry(openedFromString: CodeActionTriggerSource, didCancel: boolean, CodeActions: CodeActionSet) { + type ApplyCodeActionEvent = { + codeActionFrom: CodeActionTriggerSource; + validCodeActions: number; + cancelled: boolean; + }; + + type ApplyCodeEventClassification = { + codeActionFrom: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of action used to opened the code action.' }; + validCodeActions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The total number of valid actions that are highlighted and can be used.' }; + cancelled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The indicator if the menu was selected or cancelled.' }; + owner: 'mjbvz'; + comment: 'Event used to gain insights into how code actions are being triggered'; + }; + + this._telemetryService.publicLog2<ApplyCodeActionEvent, ApplyCodeEventClassification>('codeAction.applyCodeAction', { + codeActionFrom: openedFromString, + validCodeActions: CodeActions.validActions.length, + cancelled: didCancel, + + }); + } + public async show(trigger: CodeActionTrigger, codeActions: CodeActionSet, at: IAnchor | IPosition, options: CodeActionShowOptions): Promise<void> { + const model = this._editor.getModel(); + if (!model) { + return; + } const actionsToShow = options.includeDisabledActions ? codeActions.allActions : codeActions.validActions; if (!actionsToShow.length) { this._visible = false; @@ -100,40 +400,35 @@ export class CodeActionMenu extends Disposable { const useShadowDOM = this._editor.getOption(EditorOption.useShadowDOM); - this._contextMenuService.showContextMenu({ - domForShadowRoot: useShadowDOM ? this._editor.getDomNode()! : undefined, - getAnchor: () => anchor, - getActions: () => menuActions, - onHide: (didCancel) => { - const openedFromString = (options.fromLightbulb) ? CodeActionTriggerSource.Lightbulb : trigger.triggerAction; - - type ApplyCodeActionEvent = { - codeActionFrom: CodeActionTriggerSource; - validCodeActions: number; - cancelled: boolean; - }; - - type ApplyCodeEventClassification = { - codeActionFrom: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of action used to opened the code action.' }; - validCodeActions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The total number of valid actions that are highlighted and can be used.' }; - cancelled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The indicator if the menu was selected or cancelled.' }; - owner: 'mjbvz'; - comment: 'Event used to gain insights into how code actions are being triggered'; - }; - - this._telemetryService.publicLog2<ApplyCodeActionEvent, ApplyCodeEventClassification>('codeAction.applyCodeAction', { - codeActionFrom: openedFromString, - validCodeActions: codeActions.validActions.length, - cancelled: didCancel, - - }); - - this._visible = false; - this._editor.focus(); + + if (this.isCodeActionWidgetEnabled(model)) { + this._contextViewService.showContextView({ + getAnchor: () => anchor, + render: (container: HTMLElement) => this.renderCodeActionMenuList(container, menuActions), + onHide: (didCancel) => { + const openedFromString = (options.fromLightbulb) ? CodeActionTriggerSource.Lightbulb : trigger.triggerAction; + this.codeActionTelemetry(openedFromString, didCancel, codeActions); + this._visible = false; + this._editor.focus(); + }, }, - autoSelectFirstItem: true, - getKeyBinding: action => action instanceof CodeActionAction ? resolver(action.action) : undefined, - }); + this._editor.getDomNode()!, false, + ); + } else { + this._contextMenuService.showContextMenu({ + domForShadowRoot: useShadowDOM ? this._editor.getDomNode()! : undefined, + getAnchor: () => anchor, + getActions: () => menuActions, + onHide: (didCancel) => { + const openedFromString = (options.fromLightbulb) ? CodeActionTriggerSource.Lightbulb : trigger.triggerAction; + this.codeActionTelemetry(openedFromString, didCancel, codeActions); + this._visible = false; + this._editor.focus(); + }, + autoSelectFirstItem: true, + getKeyBinding: action => action instanceof CodeActionAction ? resolver(action.action) : undefined, + }); + } } private getMenuActions( @@ -175,7 +470,7 @@ export class CodeActionMenu extends Disposable { // Translate to absolute editor position const cursorCoords = this._editor.getScrolledVisiblePosition(position); - const editorCoords = getDomNodePagePosition(this._editor.getDomNode()); + const editorCoords = dom.getDomNodePagePosition(this._editor.getDomNode()); const x = editorCoords.left + cursorCoords.left; const y = editorCoords.top + cursorCoords.top + cursorCoords.height; diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts b/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts index 0f8cd9b237d..e42d043ab5e 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts @@ -55,6 +55,29 @@ export class CodeActionUi extends Disposable { override dispose() { this.#disposed = true; super.dispose(); + + } + + public hideCodeActionWidget() { + if (this._codeActionWidget.hasValue()) { + this._codeActionWidget.getValue().hideCodeActionWidget(); + } + } + + public onEnter() { + if (this._codeActionWidget.hasValue()) { + this._codeActionWidget.getValue().onEnterSet(); + } + } + + public navigateList(navUp: Boolean) { + if (this._codeActionWidget.hasValue()) { + if (navUp) { + this._codeActionWidget.getValue().navigateListWithKeysUp(); + } else { + this._codeActionWidget.getValue().navigateListWithKeysDown(); + } + } } public async update(newState: CodeActionsState.State): Promise<void> { diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionWidgetContribution.ts b/src/vs/editor/contrib/codeAction/browser/codeActionWidgetContribution.ts new file mode 100644 index 00000000000..3e7c2df8fda --- /dev/null +++ b/src/vs/editor/contrib/codeAction/browser/codeActionWidgetContribution.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 { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; +import * as nls from 'vs/nls'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; + +Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({ + ...editorConfigurationBaseNode, + properties: { + 'editor.experimental.useCustomCodeActionMenu': { + type: 'boolean', + tags: ['experimental'], + scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, + description: nls.localize('codeActionWidget', "Enabling this adjusts how the code action menu is rendered."), + default: false, + }, + } +}); diff --git a/src/vs/editor/contrib/codeAction/browser/media/action.css b/src/vs/editor/contrib/codeAction/browser/media/action.css new file mode 100644 index 00000000000..4068820413d --- /dev/null +++ b/src/vs/editor/contrib/codeAction/browser/media/action.css @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.codeActionMenuWidget { + padding: 8px 0px 8px 0px; + overflow: auto; + font-size: 13px; + border-radius: 5px; + min-width: 160px; + z-index: 40; + display: block; + /* flex-direction: column; + flex: 0 1 auto; */ + width: 100%; + border-width: 0px; + border-color: none; + background-color: var(--vscode-menu-background); + color: var(--vscode-menu-foreground); +} + +.codeActionMenuWidget .monaco-list:not(.element-focused):focus:before { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 5; /* make sure we are on top of the tree items */ + content: ""; + pointer-events: none; /* enable click through */ + outline: 0px solid; /* we still need to handle the empty tree or no focus item case */ + outline-width: 0px; + outline-style: none; + outline-offset: 0px; +} + +.codeActionMenuWidget .monaco-list { + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + border: none !important; + border-width: 0px !important; +} + +/* .codeActionMenuWidget .monaco-list:not(.element-focus) { + border: none !important; + border-width: 0px !important; +} */ + +.codeActionMenuWidget .monaco-list .monaco-scrollable-element .monaco-list-rows { + height: 100% !important; +} + +.codeActionMenuWidget .monaco-list .monaco-scrollable-element { + overflow: visible; +} +/** Styles for each row in the list element **/ + +.codeActionMenuWidget .monaco-list .monaco-list-row:not(.separator) { + display: flex; + -mox-box-sizing: border-box; + box-sizing: border-box; + padding: 0px 26px 0px 26px; + background-repeat: no-repeat; + background-position: 2px 2px; + white-space: nowrap; + cursor: pointer; + touch-action: none; + width: 100%; +} + + +.codeActionMenuWidget .monaco-list .monaco-list-row:hover:not(.option-disabled), +.codeActionMenuWidget .monaco-list .moncao-list-row.focused:not(.option-disabled) { + color: var(--vscode-menu-selectionForeground) !important; + background-color: var(--vscode-menu-selectionBackground) !important; +} + +.codeActionMenuWidget .monaco-list .option-disabled, +.codeActionMenuWidget .monaco-list .option-disabled .focused { + pointer-events: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + color: var(--vscode-disabledForeground) !important; +} + +.codeActionMenuWidget .monaco-list .separator { + border-bottom: 1px solid var(--vscode-menu-separatorBackground); + padding-top: 0px !important; + /* padding: 30px; */ + width: 100%; + height: 0px !important; + opacity: 1; + font-size: inherit; + margin: 5px 0 !important; + border-radius: 0; + display: flex; + -mox-box-sizing: border-box; + box-sizing: border-box; + background-repeat: no-repeat; + background-position: 2px 2px; + white-space: nowrap; + cursor: pointer; + touch-action: none; +} diff --git a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts index 0a3945bc290..0cd4c1e73b0 100644 --- a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts @@ -327,7 +327,7 @@ export class ContextMenuController implements IEditorContribution { } })); actions.push(createEnumAction<'proportional' | 'fill' | 'fit'>( - nls.localize('context.minimap.size', "Size"), + nls.localize('context.minimap.size', "Vertical size"), minimapOptions.enabled, 'editor.minimap.size', minimapOptions.size, @@ -342,22 +342,6 @@ export class ContextMenuController implements IEditorContribution { value: 'fit' }] )); - actions.push(createEnumAction<number>( - nls.localize('context.minimap.scale', "Scale"), - minimapOptions.enabled, - 'editor.minimap.scale', - minimapOptions.scale, - [{ - label: nls.localize('context.minimap.scale.1', "1"), - value: 1 - }, { - label: nls.localize('context.minimap.scale.2', "2"), - value: 2 - }, { - label: nls.localize('context.minimap.scale.3', "3"), - value: 3 - }] - )); actions.push(createEnumAction<'always' | 'mouseover'>( nls.localize('context.minimap.slider', "Slider"), minimapOptions.enabled, diff --git a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts index 551092627e5..10ed525fc05 100644 --- a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts +++ b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { raceCancellation } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { VSDataTransfer } from 'vs/base/common/dataTransfer'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -23,7 +24,9 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState'; import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; +import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -33,10 +36,11 @@ export class DropIntoEditorController extends Disposable implements IEditorContr constructor( editor: ICodeEditor, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, - @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @IProgressService private readonly _progressService: IProgressService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, ) { super(); @@ -66,35 +70,55 @@ export class DropIntoEditorController extends Disposable implements IEditorContr } const model = editor.getModel(); - const modelVersionNow = model.getVersionId(); + const initialModelVersion = model.getVersionId(); const ourDataTransfer = await this.extractDataTransferData(dragEvent); if (ourDataTransfer.size === 0) { return; } - if (editor.getModel().getVersionId() !== modelVersionNow) { + if (editor.getModel().getVersionId() !== initialModelVersion) { return; } const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value); try { const providers = this._languageFeaturesService.documentOnDropEditProvider.ordered(model); - for (const provider of providers) { - const edit = await provider.provideDocumentOnDropEdits(model, position, ourDataTransfer, tokenSource.token); - if (tokenSource.token.isCancellationRequested || editor.getModel().getVersionId() !== modelVersionNow) { - return; - } - - if (edit) { - const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); - performSnippetEdit(editor, typeof edit.insertText === 'string' ? SnippetParser.escape(edit.insertText) : edit.insertText.snippet, [Selection.fromRange(range, SelectionDirection.LTR)]); - if (edit.additionalEdit) { - await this._bulkEditService.apply(ResourceEdit.convert(edit.additionalEdit), { editor }); + const edit = await this._progressService.withProgress({ + location: ProgressLocation.Notification, + delay: 750, + title: localize('dropProgressTitle', "Running drop handlers..."), + cancellable: true, + }, () => { + return raceCancellation((async () => { + for (const provider of providers) { + const edit = await provider.provideDocumentOnDropEdits(model, position, ourDataTransfer, tokenSource.token); + if (tokenSource.token.isCancellationRequested) { + return undefined; + } + if (edit) { + return edit; + } } - return; + return undefined; + })(), tokenSource.token); + }, () => { + tokenSource.cancel(); + }); + + if (tokenSource.token.isCancellationRequested || editor.getModel().getVersionId() !== initialModelVersion) { + return; + } + + if (edit) { + const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); + performSnippetEdit(editor, typeof edit.insertText === 'string' ? SnippetParser.escape(edit.insertText) : edit.insertText.snippet, [Selection.fromRange(range, SelectionDirection.LTR)]); + + if (edit.additionalEdit) { + await this._bulkEditService.apply(ResourceEdit.convert(edit.additionalEdit), { editor }); } + return; } } finally { tokenSource.dispose(); diff --git a/src/vs/editor/contrib/links/browser/links.ts b/src/vs/editor/contrib/links/browser/links.ts index adcb05a6a1f..22b081f3931 100644 --- a/src/vs/editor/contrib/links/browser/links.ts +++ b/src/vs/editor/contrib/links/browser/links.ts @@ -248,7 +248,7 @@ export class LinkDetector extends Disposable implements IEditorContribution { } } - return this.openerService.open(uri, { openToSide, fromUserGesture, allowContributedOpeners: true, allowCommands: true }); + return this.openerService.open(uri, { openToSide, fromUserGesture, allowContributedOpeners: true, allowCommands: true, fromWorkspace: true }); }, err => { const messageOrError = diff --git a/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts b/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts index f72e2293aa7..7220dbf1a4f 100644 --- a/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts +++ b/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts @@ -10,7 +10,7 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { onUnexpectedError } from 'vs/base/common/errors'; import { tokenizeToString } from 'vs/editor/common/languages/textToHtmlTokenizer'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { DebounceEmitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; @@ -38,10 +38,7 @@ export class MarkdownRenderer { } }); - private readonly _onDidRenderAsync = new DebounceEmitter<void>({ - delay: 50, - merge: arr => { } - }); + private readonly _onDidRenderAsync = new Emitter<void>(); readonly onDidRenderAsync = this._onDidRenderAsync.event; constructor( diff --git a/src/vs/editor/contrib/message/browser/messageController.ts b/src/vs/editor/contrib/message/browser/messageController.ts index b69ceb315e7..04177380066 100644 --- a/src/vs/editor/contrib/message/browser/messageController.ts +++ b/src/vs/editor/contrib/message/browser/messageController.ts @@ -32,7 +32,6 @@ export class MessageController implements IEditorContribution { private readonly _visible: IContextKey<boolean>; private readonly _messageWidget = new MutableDisposable<MessageWidget>(); private readonly _messageListeners = new DisposableStore(); - private readonly _editorListener: IDisposable; constructor( editor: ICodeEditor, @@ -41,11 +40,9 @@ export class MessageController implements IEditorContribution { this._editor = editor; this._visible = MessageController.MESSAGE_VISIBLE.bindTo(contextKeyService); - this._editorListener = this._editor.onDidAttemptReadOnlyEdit(() => this._onDidAttemptReadOnlyEdit()); } dispose(): void { - this._editorListener.dispose(); this._messageListeners.dispose(); this._messageWidget.dispose(); this._visible.reset(); @@ -98,16 +95,6 @@ export class MessageController implements IEditorContribution { this._messageListeners.add(MessageWidget.fadeOut(this._messageWidget.value)); } } - - private _onDidAttemptReadOnlyEdit(): void { - if (this._editor.hasModel()) { - if (this._editor.isSimpleWidget) { - this.showMessage(nls.localize('editor.simple.readonly', "Cannot edit in read-only input"), this._editor.getPosition()); - } else { - this.showMessage(nls.localize('editor.readonly', "Cannot edit in read-only editor"), this._editor.getPosition()); - } - } - } } const MessageCommand = EditorCommand.bindToContribution<MessageController>(MessageController.get); diff --git a/src/vs/editor/contrib/readOnlyMessage/browser/contribution.ts b/src/vs/editor/contrib/readOnlyMessage/browser/contribution.ts new file mode 100644 index 00000000000..c4df6086148 --- /dev/null +++ b/src/vs/editor/contrib/readOnlyMessage/browser/contribution.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; +import * as nls from 'vs/nls'; + +export class ReadOnlyMessageController extends Disposable implements IEditorContribution { + + public static readonly ID = 'editor.contrib.readOnlyMessageController'; + + constructor( + private readonly editor: ICodeEditor + ) { + super(); + this._register(this.editor.onDidAttemptReadOnlyEdit(() => this._onDidAttemptReadOnlyEdit())); + } + + private _onDidAttemptReadOnlyEdit(): void { + const messageController = MessageController.get(this.editor); + if (messageController && this.editor.hasModel()) { + if (this.editor.isSimpleWidget) { + messageController.showMessage(nls.localize('editor.simple.readonly', "Cannot edit in read-only input"), this.editor.getPosition()); + } else { + messageController.showMessage(nls.localize('editor.readonly', "Cannot edit in read-only editor"), this.editor.getPosition()); + } + } + } +} + +registerEditorContribution(ReadOnlyMessageController.ID, ReadOnlyMessageController); diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 1168ef7bb16..a33146a005f 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -53,6 +53,7 @@ import 'vs/editor/contrib/viewportSemanticTokens/browser/viewportSemanticTokens' import 'vs/editor/contrib/wordHighlighter/browser/wordHighlighter'; import 'vs/editor/contrib/wordOperations/browser/wordOperations'; import 'vs/editor/contrib/wordPartOperations/browser/wordPartOperations'; +import 'vs/editor/contrib/readOnlyMessage/browser/contribution'; // Load up these strings even in VSCode, even if they are not used // in order to get them translated diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 8356bd94844..be6633e6582 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -41,7 +41,7 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; import { ILabelService, ResourceLabelFormatter, IFormatterChangeEvent } from 'vs/platform/label/common/label'; import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions } from 'vs/platform/notification/common/notification'; -import { IProgressRunner, IEditorProgressService } from 'vs/platform/progress/common/progress'; +import { IProgressRunner, IEditorProgressService, IProgressService, IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressNotificationOptions, IProgressOptions, IProgressStep, IProgressWindowOptions } from 'vs/platform/progress/common/progress'; import { ITelemetryInfo, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, IWorkspace, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, IWorkspaceFoldersWillChangeEvent, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; @@ -188,6 +188,17 @@ class StandaloneEditorProgressService implements IEditorProgressService { } } +class StandaloneProgressService implements IProgressService { + + declare readonly _serviceBrand: undefined; + + withProgress<R>(_options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, task: (progress: IProgress<IProgressStep>) => Promise<R>, onDidCancel?: ((choice?: number | undefined) => void) | undefined): Promise<R> { + return task({ + report: () => { }, + }); + } +} + class StandaloneDialogService implements IDialogService { public _serviceBrand: undefined; @@ -962,6 +973,7 @@ registerSingleton(ILogService, StandaloneLogService); registerSingleton(IModelService, ModelService); registerSingleton(IMarkerDecorationsService, MarkerDecorationsService); registerSingleton(IContextKeyService, ContextKeyService); +registerSingleton(IProgressService, StandaloneProgressService); registerSingleton(IEditorProgressService, StandaloneEditorProgressService); registerSingleton(IStorageService, InMemoryStorageService); registerSingleton(IEditorWorkerService, EditorWorkerService); diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index 493db522258..555a1f5f59a 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -747,6 +747,8 @@ suite('TextModelSearch', () => { assert(isMultilineRegexSource('foo\\r\\n')); assert(isMultilineRegexSource('\\n')); assert(isMultilineRegexSource('foo\\W')); + assert(isMultilineRegexSource('foo\n')); + assert(isMultilineRegexSource('foo\r\n')); }); test('issue #74715. \\d* finds empty string and stops searching.', () => { diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 4c98b03d76a..a12393b04cc 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -130,23 +130,6 @@ export interface IMenuEntryActionViewItemOptions { hoverDelegate?: IHoverDelegate; } -function registerConfigureMenu(contextMenuService: IContextMenuService, item: BaseActionViewItem, action: MenuItemAction | SubmenuItemAction): IDisposable { - assertType(item.element); - return addDisposableListener(item.element, 'contextmenu', event => { - if (!action.hideActions) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - - contextMenuService.showContextMenu({ - getAnchor: () => item.element!, - getActions: () => action.hideActions!.asList() - }); - }, true); -} - export class MenuEntryActionViewItem extends ActionViewItem { private _wantsAltCommand: boolean = false; @@ -221,8 +204,6 @@ export class MenuEntryActionViewItem extends ActionViewItem { mouseOver = true; updateAltState(); })); - - this._register(registerConfigureMenu(this._contextMenuService, this, this._menuItemAction)); } override updateLabel(): void { @@ -348,8 +329,6 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { setBackgroundImage(); })); } - - this._register(registerConfigureMenu(this._contextMenuService, this, action)); } } @@ -469,8 +448,6 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { event.stopPropagation(); } })); - - this._register(registerConfigureMenu(this._contextMenuService, this, (<SubmenuItemAction>this.action))); } override focus(fromRight?: boolean): void { diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index a11e1185e92..62446fbc2ca 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -164,6 +164,8 @@ export class MenuId { static readonly InlineCompletionsActions = new MenuId('InlineCompletionsActions'); static readonly NewFile = new MenuId('NewFile'); static readonly MergeToolbar = new MenuId('MergeToolbar'); + static readonly MergeInput1Toolbar = new MenuId('MergeToolbar1Toolbar'); + static readonly MergeInput2Toolbar = new MenuId('MergeToolbar2Toolbar'); /** * Create or reuse a `MenuId` with the given identifier @@ -353,7 +355,6 @@ export class SubmenuItemAction extends SubmenuAction { constructor( readonly item: ISubmenuItem, - readonly hideActions: MenuItemActionManageActions, private readonly _menuService: IMenuService, private readonly _contextKeyService: IContextKeyService, private readonly _options?: IMenuActionOptions @@ -379,20 +380,10 @@ export class SubmenuItemAction extends SubmenuAction { } } -export class MenuItemActionManageActions { - constructor( - readonly hideThis: IAction, - readonly toggleAny: readonly IAction[][], - ) { } - - asList(): IAction[] { - let result: IAction[] = [this.hideThis]; - for (const n of this.toggleAny) { - result.push(new Separator()); - result = result.concat(n); - } - return result; - } +export interface IMenuItemHide { + readonly isHidden: boolean; + readonly hide: IAction; + readonly toggle: IAction; } // implements IAction, does NOT extend Action, so that no one @@ -415,7 +406,7 @@ export class MenuItemAction implements IAction { item: ICommandAction, alt: ICommandAction | undefined, options: IMenuActionOptions | undefined, - readonly hideActions: MenuItemActionManageActions | undefined, + readonly hideActions: IMenuItemHide | undefined, @IContextKeyService contextKeyService: IContextKeyService, @ICommandService private _commandService: ICommandService ) { diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index fce79d84ad9..44f48f3c46c 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -6,11 +6,11 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IMenu, IMenuActionOptions, IMenuCreateOptions, IMenuItem, IMenuService, isIMenuItem, isISubmenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuItemActionManageActions, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenu, IMenuActionOptions, IMenuCreateOptions, IMenuItem, IMenuItemHide, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandAction, ILocalizedString } from 'vs/platform/action/common/action'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IAction, SubmenuAction } from 'vs/base/common/actions'; +import { IAction, toAction } from 'vs/base/common/actions'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { removeFastWithoutKeepingOrder } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; @@ -252,33 +252,17 @@ class Menu implements IMenu { if (this._contextKeyService.contextMatchesRules(item.when)) { let action: MenuItemAction | SubmenuItemAction | undefined; const isMenuItem = isIMenuItem(item); - const hideActions = new MenuItemActionManageActions(new HideMenuItemAction(this._id, isMenuItem ? item.command : item, this._hiddenStates), allToggleActions); if (isMenuItem) { - if (!this._hiddenStates.isHidden(this._id, item.command.id)) { - action = new MenuItemAction(item.command, item.alt, options, hideActions, this._contextKeyService, this._commandService); - } - // add toggle commmand - toggleActions.push(new ToggleMenuItemAction(this._id, item.command, this._hiddenStates)); + const menuHide = createMenuHide(this._id, item.command, this._hiddenStates); + action = new MenuItemAction(item.command, item.alt, options, menuHide, this._contextKeyService, this._commandService); + } else { - action = new SubmenuItemAction(item, hideActions, this._menuService, this._contextKeyService, options); + action = new SubmenuItemAction(item, this._menuService, this._contextKeyService, options); if (action.actions.length === 0) { action.dispose(); action = undefined; } - // add toggle submenu - this re-creates ToggleMenuItemAction-instances for submenus but that's OK... - if (action) { - const makeToggleCommand = (id: MenuId, action: IAction): IAction => { - if (action instanceof SubmenuItemAction) { - return new SubmenuAction(action.id, action.label, action.actions.map(a => makeToggleCommand(action.item.submenu, a))); - } else if (action instanceof MenuItemAction) { - return new ToggleMenuItemAction(id, action.item, this._hiddenStates); - } else { - return action; - } - }; - toggleActions.push(makeToggleCommand(this._id, action)); - } } if (action) { @@ -355,55 +339,30 @@ class Menu implements IMenu { } } -class ToggleMenuItemAction implements IAction { - - readonly id: string; - readonly label: string; - readonly enabled: boolean = true; - readonly tooltip: string = ''; - - readonly checked: boolean; - readonly class: undefined; - - run: () => void; - - constructor(id: MenuId, command: ICommandAction, hiddenStates: PersistedMenuHideState) { - this.id = `toggle/${id.id}/${command.id}`; - this.label = typeof command.title === 'string' ? command.title : command.title.value; - - let isHidden = hiddenStates.isHidden(id, command.id); - this.checked = !isHidden; - this.run = () => { - isHidden = !isHidden; - hiddenStates.updateHidden(id, command.id, isHidden); - }; - } - - dispose(): void { - // NOTHING - } -} - -class HideMenuItemAction implements IAction { +function createMenuHide(menu: MenuId, command: ICommandAction, states: PersistedMenuHideState): IMenuItemHide { - readonly id: string; - readonly label: string; - readonly enabled: boolean = true; - readonly tooltip: string = ''; + const id = `${menu.id}/${command.id}`; + const title = typeof command.title === 'string' ? command.title : command.title.value; - readonly checked: undefined; - readonly class: undefined; + const hide = toAction({ + id, + label: localize('hide.label', 'Hide \'{0}\'', title), + run() { states.updateHidden(menu, command.id, true); } + }); - run: () => void; - - constructor(menu: MenuId, command: ICommandAction | ISubmenuItem, hiddenStates: PersistedMenuHideState) { - const id = isISubmenuItem(command) ? command.submenu.id : command.id; - this.id = `hide/${menu.id}/${id}`; - this.label = localize('hide.label', 'Hide \'{0}\'', typeof command.title === 'string' ? command.title : command.title.value); - this.run = () => { hiddenStates.updateHidden(menu, id, true); }; - } + const toggle = toAction({ + id, + label: title, + get checked() { return !states.isHidden(menu, command.id); }, + run() { + const newValue = !states.isHidden(menu, command.id); + states.updateHidden(menu, command.id, newValue); + } + }); - dispose(): void { - // NOTHING - } + return { + hide, + toggle, + get isHidden() { return !toggle.checked; }, + }; } diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index b1800765cb0..f522354dca8 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -140,16 +140,13 @@ flakySuite('BackupMainService', () => { return pfs.Promises.rm(testDir); }); - test('service validates backup workspaces on startup and cleans up (folder workspaces) (1)', async function () { + test('service validates backup workspaces on startup and cleans up (folder workspaces)', async function () { // 1) backup workspace path does not exist service.registerFolderBackupSync(toFolderBackupInfo(fooFile)); service.registerFolderBackupSync(toFolderBackupInfo(barFile)); await service.initialize(); assertEqualFolderInfos(service.getFolderBackupPaths(), []); - }); - - test('service validates backup workspaces on startup and cleans up (folder workspaces) (2)', async function () { // 2) backup workspace path exists with empty contents within fs.mkdirSync(service.toBackupPath(fooFile)); @@ -160,9 +157,6 @@ flakySuite('BackupMainService', () => { assertEqualFolderInfos(service.getFolderBackupPaths(), []); assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); assert.ok(!fs.existsSync(service.toBackupPath(barFile))); - }); - - test('service validates backup workspaces on startup and cleans up (folder workspaces) (3)', async function () { // 3) backup workspace path exists with empty folders within fs.mkdirSync(service.toBackupPath(fooFile)); @@ -175,9 +169,6 @@ flakySuite('BackupMainService', () => { assertEqualFolderInfos(service.getFolderBackupPaths(), []); assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); assert.ok(!fs.existsSync(service.toBackupPath(barFile))); - }); - - test('service validates backup workspaces on startup and cleans up (folder workspaces) (4)', async function () { // 4) backup workspace path points to a workspace that no longer exists // so it should convert the backup worspace to an empty workspace backup @@ -194,16 +185,13 @@ flakySuite('BackupMainService', () => { assert.strictEqual(service.getEmptyWindowBackupPaths().length, 1); }); - test('service validates backup workspaces on startup and cleans up (root workspaces) (1)', async function () { + test('service validates backup workspaces on startup and cleans up (root workspaces)', async function () { // 1) backup workspace path does not exist service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(fooFile.fsPath)); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(barFile.fsPath)); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('service validates backup workspaces on startup and cleans up (root workspaces) (2)', async function () { // 2) backup workspace path exists with empty contents within fs.mkdirSync(service.toBackupPath(fooFile)); @@ -214,9 +202,6 @@ flakySuite('BackupMainService', () => { assert.deepStrictEqual(service.getWorkspaceBackups(), []); assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); assert.ok(!fs.existsSync(service.toBackupPath(barFile))); - }); - - test('service validates backup workspaces on startup and cleans up (root workspaces) (3)', async function () { // 3) backup workspace path exists with empty folders within fs.mkdirSync(service.toBackupPath(fooFile)); @@ -229,9 +214,6 @@ flakySuite('BackupMainService', () => { assert.deepStrictEqual(service.getWorkspaceBackups(), []); assert.ok(!fs.existsSync(service.toBackupPath(fooFile))); assert.ok(!fs.existsSync(service.toBackupPath(barFile))); - }); - - test('service validates backup workspaces on startup and cleans up (root workspaces) (4)', async function () { // 4) backup workspace path points to a workspace that no longer exists // so it should convert the backup worspace to an empty workspace backup @@ -291,19 +273,13 @@ flakySuite('BackupMainService', () => { assertEqualFolderInfos(service.getFolderBackupPaths(), []); }); - test('getFolderBackupPaths() should return [] when workspaces.json is not properly formed JSON (1)', async () => { + test('getFolderBackupPaths() should return [] when workspaces.json is not properly formed JSON', async () => { fs.writeFileSync(backupWorkspacesPath, ''); await service.initialize(); assertEqualFolderInfos(service.getFolderBackupPaths(), []); - }); - - test('getFolderBackupPaths() should return [] when workspaces.json is not properly formed JSON (2)', async () => { fs.writeFileSync(backupWorkspacesPath, '{]'); await service.initialize(); assertEqualFolderInfos(service.getFolderBackupPaths(), []); - }); - - test('getFolderBackupPaths() should return [] when workspaces.json is not properly formed JSON (3)', async () => { fs.writeFileSync(backupWorkspacesPath, 'foo'); await service.initialize(); assertEqualFolderInfos(service.getFolderBackupPaths(), []); @@ -315,37 +291,22 @@ flakySuite('BackupMainService', () => { assertEqualFolderInfos(service.getFolderBackupPaths(), []); }); - test('getFolderBackupPaths() should return [] when folderWorkspaceInfos in workspaces.json is not a string array (1)', async () => { + test('getFolderBackupPaths() should return [] when folderWorkspaceInfos in workspaces.json is not a string array', async () => { fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":{}}'); await service.initialize(); assertEqualFolderInfos(service.getFolderBackupPaths(), []); - }); - - test('getFolderBackupPaths() should return [] when folderWorkspaceInfos in workspaces.json is not a string array (2)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":{"foo": ["bar"]}}'); await service.initialize(); assertEqualFolderInfos(service.getFolderBackupPaths(), []); - }); - - test('getFolderBackupPaths() should return [] when folderWorkspaceInfos in workspaces.json is not a string array (3)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":{"foo": []}}'); await service.initialize(); assertEqualFolderInfos(service.getFolderBackupPaths(), []); - }); - - test('getFolderBackupPaths() should return [] when folderWorkspaceInfos in workspaces.json is not a string array (4)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":{"foo": "bar"}}'); await service.initialize(); assertEqualFolderInfos(service.getFolderBackupPaths(), []); - }); - - test('getFolderBackupPaths() should return [] when folderWorkspaceInfos in workspaces.json is not a string array (5)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":"foo"}'); await service.initialize(); assertEqualFolderInfos(service.getFolderBackupPaths(), []); - }); - - test('getFolderBackupPaths() should return [] when folderWorkspaceInfos in workspaces.json is not a string array (6)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"folderWorkspaceInfos":1}'); await service.initialize(); assertEqualFolderInfos(service.getFolderBackupPaths(), []); @@ -372,19 +333,13 @@ flakySuite('BackupMainService', () => { assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); - test('getWorkspaceBackups() should return [] when workspaces.json is not properly formed JSON (1)', async () => { + test('getWorkspaceBackups() should return [] when workspaces.json is not properly formed JSON', async () => { fs.writeFileSync(backupWorkspacesPath, ''); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when workspaces.json is not properly formed JSON (2)', async () => { fs.writeFileSync(backupWorkspacesPath, '{]'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when workspaces.json is not properly formed JSON (3)', async () => { fs.writeFileSync(backupWorkspacesPath, 'foo'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); @@ -396,73 +351,43 @@ flakySuite('BackupMainService', () => { assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); - test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array (1)', async () => { + test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{}}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array (2)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array (3)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": []}}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array (4)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":{"foo": "bar"}}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array (5)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":"foo"}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when rootWorkspaces in workspaces.json is not a object array (6)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootWorkspaces":1}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); }); - test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array (1)', async () => { + test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{}}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array (2)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array (3)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": []}}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array (4)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":{"foo": "bar"}}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array (5)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":"foo"}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); - }); - - test('getWorkspaceBackups() should return [] when rootURIWorkspaces in workspaces.json is not a object array (6)', async () => { fs.writeFileSync(backupWorkspacesPath, '{"rootURIWorkspaces":1}'); await service.initialize(); assert.deepStrictEqual(service.getWorkspaceBackups(), []); @@ -482,19 +407,13 @@ flakySuite('BackupMainService', () => { assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); - test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json is not properly formed JSON (1)', async () => { + test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json is not properly formed JSON', async () => { fs.writeFileSync(backupWorkspacesPath, ''); await service.initialize(); assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); - }); - - test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json is not properly formed JSON (2)', async () => { fs.writeFileSync(backupWorkspacesPath, '{]'); await service.initialize(); assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); - }); - - test('getEmptyWorkspaceBackupPaths() should return [] when workspaces.json is not properly formed JSON (3)', async () => { fs.writeFileSync(backupWorkspacesPath, 'foo'); await service.initialize(); assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); @@ -506,37 +425,22 @@ flakySuite('BackupMainService', () => { assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); }); - test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array (1)', async function () { + test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array', async function () { fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{}}'); await service.initialize(); assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); - }); - - test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array (2)', async function () { fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": ["bar"]}}'); await service.initialize(); assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); - }); - - test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array (3)', async function () { fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": []}}'); await service.initialize(); assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); - }); - - test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array (4)', async function () { fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":{"foo": "bar"}}'); await service.initialize(); assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); - }); - - test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array (5)', async function () { fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":"foo"}'); await service.initialize(); assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); - }); - - test('getEmptyWorkspaceBackupPaths() should return [] when folderWorkspaces in workspaces.json is not a string array (6)', async function () { fs.writeFileSync(backupWorkspacesPath, '{"emptyWorkspaces":1}'); await service.initialize(); assert.deepStrictEqual(service.getEmptyWindowBackupPaths(), []); diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index 5c27c738322..4ea7a9bce8d 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -34,7 +34,8 @@ export function isConfigurationUpdateOverrides(thing: any): thing is IConfigurat export type IConfigurationUpdateOverrides = Omit<IConfigurationOverrides, 'overrideIdentifier'> & { overrideIdentifiers?: string[] | null }; export const enum ConfigurationTarget { - USER = 1, + APPLICATION = 1, + USER, USER_LOCAL, USER_REMOTE, WORKSPACE, @@ -44,6 +45,7 @@ export const enum ConfigurationTarget { } export function ConfigurationTargetToString(configurationTarget: ConfigurationTarget) { switch (configurationTarget) { + case ConfigurationTarget.APPLICATION: return 'APPLICATION'; case ConfigurationTarget.USER: return 'USER'; case ConfigurationTarget.USER_LOCAL: return 'USER_LOCAL'; case ConfigurationTarget.USER_REMOTE: return 'USER_REMOTE'; diff --git a/src/vs/platform/contextview/browser/contextViewService.ts b/src/vs/platform/contextview/browser/contextViewService.ts index 57be989bcc4..3a2d4a47dbc 100644 --- a/src/vs/platform/contextview/browser/contextViewService.ts +++ b/src/vs/platform/contextview/browser/contextViewService.ts @@ -14,6 +14,7 @@ export class ContextViewService extends Disposable implements IContextViewServic private currentViewDisposable: IDisposable = Disposable.None; private contextView: ContextView; private container: HTMLElement | null; + private shadowRoot: boolean | undefined; constructor( @ILayoutService readonly layoutService: ILayoutService @@ -35,7 +36,7 @@ export class ContextViewService extends Disposable implements IContextViewServic showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IDisposable { if (container) { - if (container !== this.container) { + if (container !== this.container || this.shadowRoot !== shadowRoot) { this.container = container; this.setContainer(container, shadowRoot ? ContextViewDOMPosition.FIXED_SHADOW : ContextViewDOMPosition.FIXED); } @@ -46,6 +47,8 @@ export class ContextViewService extends Disposable implements IContextViewServic } } + this.shadowRoot = shadowRoot; + this.contextView.show(delegate); const disposable = toDisposable(() => { diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 5424707ace6..71d2d312250 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -90,7 +90,7 @@ export interface NativeParsedArgs { 'logsPath'?: string; '__enable-file-policy'?: boolean; editSessionId?: string; - 'shell-integration'?: string; + 'locate-shell-integration-path'?: string; // chromium command line args: https://electronjs.org/docs/all#supported-chrome-command-line-switches 'no-proxy-server'?: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 68769c3d547..1728cfec19a 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -50,7 +50,6 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = { 'waitMarkerFilePath': { type: 'string' }, 'locale': { type: 'string', cat: 'o', args: 'locale', description: localize('locale', "The locale to use (e.g. en-US or zh-TW).") }, 'user-data-dir': { type: 'string', cat: 'o', args: 'dir', description: localize('userDataDir', "Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of Code.") }, - 'shell-integration': { type: 'string', cat: 'o', args: ['bash', 'pwsh', 'zsh'], description: localize('shellIntergation', "Print the shell integration script file path for the specified shell.") }, 'help': { type: 'boolean', cat: 'o', alias: 'h', description: localize('help', "Print usage.") }, 'extensions-dir': { type: 'string', deprecates: ['extensionHomePath'], cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") }, @@ -129,6 +128,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = { 'logsPath': { type: 'string' }, '__enable-file-policy': { type: 'boolean' }, 'editSessionId': { type: 'string' }, + 'locate-shell-integration-path': { type: 'string', args: ['bash', 'pwsh', 'zsh'] }, // chromium flags 'no-proxy-server': { type: 'boolean' }, diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index adb137c66f7..bdb963ec6d2 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -101,7 +101,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl async uninstall(extension: ILocalExtension, options: ServerUninstallOptions = {}): Promise<void> { this.logService.trace('ExtensionManagementService#uninstall', extension.identifier.id); - return this.unininstallExtension(extension, options); + return this.uninstallExtension(extension, options); } async reinstallFromGallery(extension: ILocalExtension): Promise<void> { @@ -423,8 +423,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return compatibleExtension; } - private async unininstallExtension(extension: ILocalExtension, options: ServerUninstallOptions): Promise<void> { - const getUninstallExtensionTaskKey = (identifier: IExtensionIdentifier) => `${identifier.id.toLowerCase()}${options.profileLocation ? `@${options.profileLocation.toString()}` : ''}`; + private async uninstallExtension(extension: ILocalExtension, options: ServerUninstallOptions): Promise<void> { + const getUninstallExtensionTaskKey = (identifier: IExtensionIdentifier) => `${identifier.id.toLowerCase()}${options.versionOnly ? `-${extension.manifest.version}` : ''}${options.profileLocation ? `@${options.profileLocation.toString()}` : ''}`; const uninstallExtensionTask = this.uninstallingExtensions.get(getUninstallExtensionTaskKey(extension.identifier)); if (uninstallExtensionTask) { this.logService.info('Extensions is already requested to uninstall', extension.identifier.id); diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 115348c255f..b51075daec1 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -278,7 +278,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } private dedupExtensions(system: IScannedExtension[] | undefined, user: IScannedExtension[] | undefined, development: IScannedExtension[] | undefined, targetPlatform: TargetPlatform, pickLatest: boolean): IScannedExtension[] { - const pick = (existing: IScannedExtension, extension: IScannedExtension): boolean => { + const pick = (existing: IScannedExtension, extension: IScannedExtension, isDevelopment: boolean): boolean => { if (existing.isValid && !extension.isValid) { return false; } @@ -298,10 +298,10 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } } } - if (existing.type === ExtensionType.System) { - this.logService.debug(`Overwriting system extension ${existing.location.path} with ${extension.location.path}.`); - } else { + if (isDevelopment) { this.logService.warn(`Overwriting user extension ${existing.location.path} with ${extension.location.path}.`); + } else { + this.logService.debug(`Overwriting user extension ${existing.location.path} with ${extension.location.path}.`); } return true; }; @@ -309,7 +309,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem system?.forEach((extension) => { const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id); const existing = result.get(extensionKey); - if (!existing || pick(existing, extension)) { + if (!existing || pick(existing, extension, false)) { result.set(extensionKey, extension); } }); @@ -320,14 +320,14 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem this.logService.debug(`Skipping obsolete system extension ${extension.location.path}.`); return; } - if (!existing || pick(existing, extension)) { + if (!existing || pick(existing, extension, false)) { result.set(extensionKey, extension); } }); development?.forEach(extension => { const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id); const existing = result.get(extensionKey); - if (!existing || pick(existing, extension)) { + if (!existing || pick(existing, extension, true)) { result.set(extensionKey, extension); } result.set(extensionKey, extension); diff --git a/src/vs/platform/files/test/node/diskFileService.test.ts b/src/vs/platform/files/test/node/diskFileService.test.ts index c76b85fe292..d6cf5c76fa0 100644 --- a/src/vs/platform/files/test/node/diskFileService.test.ts +++ b/src/vs/platform/files/test/node/diskFileService.test.ts @@ -142,8 +142,6 @@ flakySuite('Disk File Service', function () { const disposables = new DisposableStore(); setup(async () => { - DiskFileSystemProvider.configureFlushOnWrite(true); // but enable flushing for the purpose of these tests - const logService = new NullLogService(); service = new FileService(logService); @@ -164,14 +162,10 @@ flakySuite('Disk File Service', function () { await Promises.copy(sourceDir, testDir, { preserveSymlinks: false }); }); - teardown(async () => { - try { - disposables.clear(); + teardown(() => { + disposables.clear(); - await Promises.rm(testDir); - } finally { - DiskFileSystemProvider.configureFlushOnWrite(false); - } + return Promises.rm(testDir); }); test('createFolder', async () => { @@ -1802,6 +1796,15 @@ flakySuite('Disk File Service', function () { return testWriteFile(); }); + test('writeFile - flush on write', async () => { + DiskFileSystemProvider.configureFlushOnWrite(true); + try { + return await testWriteFile(); + } finally { + DiskFileSystemProvider.configureFlushOnWrite(false); + } + }); + test('writeFile - buffered', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index e5b6b8a1045..b9ee570c43e 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -51,7 +51,7 @@ export interface ResourceLabelFormatter { } export interface ResourceLabelFormatting { - label: string; // myLabel:/${path} + label: string | ((resource: URI) => string); // myLabel:/${path} separator: '/' | '\\' | ''; tildify?: boolean; normalizeDriveLetter?: boolean; diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index eca74c211c7..9716ea6dc75 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -7,10 +7,10 @@ import { createStyleSheet } from 'vs/base/browser/dom'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { IListMouseEvent, IListRenderer, IListTouchEvent, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedListOptions, IPagedRenderer, PagedList } from 'vs/base/browser/ui/list/listPaging'; -import { DefaultStyleController, IListAccessibilityProvider, IListOptions, IListOptionsUpdate, IMultipleSelectionController, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent, List, TypeNavigationMode } from 'vs/base/browser/ui/list/listWidget'; +import { DefaultStyleController, IKeyboardNavigationEventFilter, IListAccessibilityProvider, IListOptions, IListOptionsUpdate, IMultipleSelectionController, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent, List, TypeNavigationMode } from 'vs/base/browser/ui/list/listWidget'; import { ITableColumn, ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { ITableOptions, ITableOptionsUpdate, Table } from 'vs/base/browser/ui/table/tableWidget'; -import { TreeFindMode, IAbstractTreeOptions, IAbstractTreeOptionsUpdate, IKeyboardNavigationEventFilter, RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; +import { TreeFindMode, IAbstractTreeOptions, IAbstractTreeOptionsUpdate, RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; import { AsyncDataTree, CompressibleAsyncDataTree, IAsyncDataTreeOptions, IAsyncDataTreeOptionsUpdate, ICompressibleAsyncDataTreeOptions, ICompressibleAsyncDataTreeOptionsUpdate, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { DataTree, IDataTreeOptions } from 'vs/base/browser/ui/tree/dataTree'; import { CompressibleObjectTree, ICompressibleObjectTreeOptions, ICompressibleObjectTreeOptionsUpdate, ICompressibleTreeRenderer, IObjectTreeOptions, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; @@ -187,7 +187,14 @@ class MultipleSelectionController<T> extends Disposable implements IMultipleSele } } -function toWorkbenchListOptions<T>(options: IListOptions<T>, configurationService: IConfigurationService, keybindingService: IKeybindingService): [IListOptions<T>, IDisposable] { +function toWorkbenchListOptions<T>( + accessor: ServicesAccessor, + container: HTMLElement, + options: IListOptions<T>, +): [IListOptions<T>, IDisposable] { + const configurationService = accessor.get(IConfigurationService); + const keybindingService = accessor.get(IKeybindingService); + const disposables = new DisposableStore(); const result: IListOptions<T> = { ...options, @@ -195,7 +202,8 @@ function toWorkbenchListOptions<T>(options: IListOptions<T>, configurationServic smoothScrolling: Boolean(configurationService.getValue(listSmoothScrolling)), mouseWheelScrollSensitivity: configurationService.getValue<number>(mouseWheelScrollSensitivityKey), fastScrollSensitivity: configurationService.getValue<number>(fastScrollSensitivityKey), - multipleSelectionController: options.multipleSelectionController ?? disposables.add(new MultipleSelectionController(configurationService)) + multipleSelectionController: options.multipleSelectionController ?? disposables.add(new MultipleSelectionController(configurationService)), + keyboardNavigationEventFilter: createKeyboardNavigationEventFilter(container, keybindingService), }; return [result, disposables]; @@ -233,10 +241,10 @@ export class WorkbenchList<T> extends List<T> { @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService + @IInstantiationService instantiationService: IInstantiationService ) { const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); - const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); + const [workbenchListOptions, workbenchListOptionsDisposable] = instantiationService.invokeFunction(toWorkbenchListOptions, container, options); super(user, container, delegate, renderers, { @@ -373,10 +381,10 @@ export class WorkbenchPagedList<T> extends PagedList<T> { @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService + @IInstantiationService instantiationService: IInstantiationService ) { const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); - const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); + const [workbenchListOptions, workbenchListOptionsDisposable] = instantiationService.invokeFunction(toWorkbenchListOptions, container, options); super(user, container, delegate, renderers, { keyboardSupport: false, @@ -506,10 +514,10 @@ export class WorkbenchTable<TRow> extends Table<TRow> { @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService + @IInstantiationService instantiationService: IInstantiationService ) { const horizontalScrolling = typeof options.horizontalScrolling !== 'undefined' ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); - const [workbenchListOptions, workbenchListOptionsDisposable] = toWorkbenchListOptions(options, configurationService, keybindingService); + const [workbenchListOptions, workbenchListOptionsDisposable] = instantiationService.invokeFunction(toWorkbenchListOptions, container, options); super(user, container, delegate, columns, renderers, { @@ -819,13 +827,13 @@ function createKeyboardNavigationEventFilter(container: HTMLElement, keybindingS const result = keybindingService.softDispatch(event, container); - if (result && result.enterChord) { + if (result?.enterChord) { inChord = true; return false; } inChord = false; - return true; + return !result; }; } @@ -1076,6 +1084,7 @@ function workbenchTreeDataPreamble<T, TFilterData, TOptions extends IAbstractTre const keybindingService = accessor.get(IKeybindingService); const contextViewService = accessor.get(IContextViewService); const contextKeyService = accessor.get(IContextKeyService); + const instantiationService = accessor.get(IInstantiationService); const getTypeNavigationMode = () => { // give priority to the context key value to specify a value @@ -1098,7 +1107,7 @@ function workbenchTreeDataPreamble<T, TFilterData, TOptions extends IAbstractTre }; const horizontalScrolling = options.horizontalScrolling !== undefined ? options.horizontalScrolling : Boolean(configurationService.getValue(horizontalScrollingKey)); - const [workbenchListOptions, disposable] = toWorkbenchListOptions(options, configurationService, keybindingService); + const [workbenchListOptions, disposable] = instantiationService.invokeFunction(toWorkbenchListOptions, container, options); const additionalScrollHeight = options.additionalScrollHeight; return { diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 5fd1d4d738c..a4b9b303b87 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -219,8 +219,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const window = this.windowById(windowId); if (window?.win) { window.win.setTitleBarOverlay({ - color: options.backgroundColor, - symbolColor: options.foregroundColor, + color: options.backgroundColor?.trim() === '' ? undefined : options.backgroundColor, + symbolColor: options.foregroundColor?.trim() === '' ? undefined : options.foregroundColor, height: options.height ? options.height - 1 : undefined // account for window border }); } diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 50c312b8bf3..ce203854888 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -41,6 +41,7 @@ export type OpenExternalOptions = { readonly openExternal?: boolean; readonly allowTunneling?: boolean; readonly allowContributedOpeners?: boolean | string; + readonly fromWorkspace?: boolean; }; export type OpenOptions = OpenInternalOptions & OpenExternalOptions; @@ -61,7 +62,7 @@ export interface IExternalOpener { } export interface IValidator { - shouldOpen(resource: URI | string): Promise<boolean>; + shouldOpen(resource: URI | string, openOptions?: OpenOptions): Promise<boolean>; } export interface IExternalUriResolver { diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 3b00369640a..adee9697946 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -75,6 +75,7 @@ export interface IProgressDialogOptions extends IProgressOptions { export interface IProgressWindowOptions extends IProgressOptions { readonly location: ProgressLocation.Window; readonly command?: string; + readonly type?: 'syncing' | 'loading'; } export interface IProgressCompositeOptions extends IProgressOptions { diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 15f9d6d94d0..ede2afcf7f3 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -87,7 +87,7 @@ function registerProxyConfigurations(scope: ConfigurationScope): void { properties: { 'http.proxy': { type: 'string', - pattern: '^https?://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$', + pattern: '^(https?|socks5?)://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$', markdownDescription: localize('proxy', "The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables."), restricted: true }, diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 7194815abb1..f4b242a0c69 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -11,7 +11,7 @@ import { ICommandDetectionCapability, TerminalCapability, ITerminalCommand, IHan import { ISerializedCommand, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; // Importing types is safe in any layer // eslint-disable-next-line code-import-patterns -import type { IBuffer, IDisposable, IMarker, Terminal } from 'xterm-headless'; +import type { IBuffer, IBufferLine, IDisposable, IMarker, Terminal } from 'xterm-headless'; export interface ICurrentPartialCommand { previousCommandMarker?: IMarker; @@ -601,8 +601,13 @@ function getOutputForCommand(executedMarker: IMarker | undefined, endMarker: IMa return undefined; } let output = ''; + let line: IBufferLine | undefined; for (let i = startLine; i < endLine; i++) { - output += buffer.getLine(i)?.translateToString(true) + '\n'; + line = buffer.getLine(i); + if (!line) { + continue; + } + output += line.translateToString(!line.isWrapped) + (line.isWrapped ? '' : '\n'); } return output === '' ? undefined : output; } diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index b26f20b529d..3305267657d 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -266,7 +266,7 @@ export const checkboxForeground = registerColor('checkbox.foreground', { dark: s export const checkboxBorder = registerColor('checkbox.border', { dark: selectBorder, light: selectBorder, hcDark: selectBorder, hcLight: selectBorder }, nls.localize('checkbox.border', "Border color of checkbox widget.")); export const buttonForeground = registerColor('button.foreground', { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: Color.white }, nls.localize('buttonForeground', "Button foreground color.")); -export const buttonSeparator = registerColor('button.separator', { dark: transparent(buttonForeground, .4), light: transparent(buttonForeground, .4), hcDark: null, hcLight: transparent(buttonForeground, .4) }, nls.localize('buttonSeparator', "Button separator color.")); +export const buttonSeparator = registerColor('button.separator', { dark: transparent(buttonForeground, .4), light: transparent(buttonForeground, .4), hcDark: transparent(buttonForeground, .4), hcLight: transparent(buttonForeground, .4) }, nls.localize('buttonSeparator', "Button separator color.")); export const buttonBackground = registerColor('button.background', { dark: '#0E639C', light: '#007ACC', hcDark: null, hcLight: '#0F4A85' }, nls.localize('buttonBackground', "Button background color.")); export const buttonHoverBackground = registerColor('button.hoverBackground', { dark: lighten(buttonBackground, 0.2), light: darken(buttonBackground, 0.2), hcDark: null, hcLight: null }, nls.localize('buttonHoverBackground', "Button background color when hovering.")); export const buttonBorder = registerColor('button.border', { dark: contrastBorder, light: contrastBorder, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('buttonBorder', "Button border color.")); @@ -304,10 +304,8 @@ export const sashHoverBorder = registerColor('sash.hoverBorder', { dark: focusBo /** * Editor background color. - * Because of bug https://monacotools.visualstudio.com/DefaultCollection/Monaco/_workitems/edit/13254 - * we are *not* using the color white (or #ffffff, rgba(255,255,255)) but something very close to white. */ -export const editorBackground = registerColor('editor.background', { light: '#fffffe', dark: '#1E1E1E', hcDark: Color.black, hcLight: Color.white }, nls.localize('editorBackground', "Editor background color.")); +export const editorBackground = registerColor('editor.background', { light: '#ffffff', dark: '#1E1E1E', hcDark: Color.black, hcLight: Color.white }, nls.localize('editorBackground', "Editor background color.")); /** * Editor foreground color. diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 9d8a09a7e89..7cd4a84084e 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -92,4 +92,5 @@ export interface IUpdateService { quitAndInstall(): Promise<void>; isLatestVersion(): Promise<boolean | undefined>; + _applySpecificUpdate(packagePath: string): Promise<void>; } diff --git a/src/vs/platform/update/common/updateIpc.ts b/src/vs/platform/update/common/updateIpc.ts index 55369d4a098..20a1041730e 100644 --- a/src/vs/platform/update/common/updateIpc.ts +++ b/src/vs/platform/update/common/updateIpc.ts @@ -27,6 +27,7 @@ export class UpdateChannel implements IServerChannel { case 'quitAndInstall': return this.service.quitAndInstall(); case '_getInitialState': return Promise.resolve(this.service.state); case 'isLatestVersion': return this.service.isLatestVersion(); + case '_applySpecificUpdate': return this.service._applySpecificUpdate(arg); } throw new Error(`Call not found: ${command}`); @@ -71,4 +72,8 @@ export class UpdateChannelClient implements IUpdateService { isLatestVersion(): Promise<boolean> { return this.channel.call('isLatestVersion'); } + + _applySpecificUpdate(packagePath: string): Promise<void> { + return this.channel.call('_applySpecificUpdate', packagePath); + } } diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index b451afc5eb2..0658b198141 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -59,7 +59,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<void> { if (!this.environmentMainService.isBuilt) { return; // updates are never enabled when running out of sources } @@ -201,6 +201,10 @@ export abstract class AbstractUpdateService implements IUpdateService { return context.res.statusCode === 204; } + async _applySpecificUpdate(packagePath: string): Promise<void> { + // noop + } + protected getUpdateType(): UpdateType { return UpdateType.Archive; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 82802b4830f..c5a8502f8dc 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<void> { + 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.snap.ts b/src/vs/platform/update/electron-main/updateService.snap.ts index 9d202c60aa8..cf54be65d45 100644 --- a/src/vs/platform/update/electron-main/updateService.snap.ts +++ b/src/vs/platform/update/electron-main/updateService.snap.ts @@ -129,6 +129,11 @@ abstract class AbstractUpdateService implements IUpdateService { } abstract isLatestVersion(): Promise<boolean | undefined>; + + async _applySpecificUpdate(packagePath: string): Promise<void> { + // noop + } + protected abstract doCheckForUpdates(context: any): void; } diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index fa2490cdfca..caecd719334 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -71,6 +71,15 @@ export class Win32UpdateService extends AbstractUpdateService { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); } + override async initialize(): Promise<void> { + if (this.productService.target === 'user' && await this.nativeHostMainService.isAdmin(undefined)) { + this.logService.info('update#ctor - updates are disabled due to running as Admin in user setup'); + return; + } + + super.initialize(); + } + protected buildUpdateFeedUrl(quality: string): string | undefined { let platform = 'win32'; @@ -242,4 +251,26 @@ export class Win32UpdateService extends AbstractUpdateService { protected override getUpdateType(): UpdateType { return getUpdateType(); } + + override async _applySpecificUpdate(packagePath: string): Promise<void> { + if (this.state.type !== StateType.Idle) { + return; + } + + const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); + const update: IUpdate = { version: 'unknown', productVersion: 'unknown', supportsFastUpdate: !!fastUpdatesEnabled }; + + this.setState(State.Downloading(update)); + this.availableUpdate = { packagePath }; + + if (fastUpdatesEnabled) { + if (this.productService.target === 'user') { + this.doApplyUpdate(); + } else { + this.setState(State.Downloaded(update)); + } + } else { + this.setState(State.Ready(update)); + } + } } diff --git a/src/vs/platform/userDataProfile/browser/userDataProfile.ts b/src/vs/platform/userDataProfile/browser/userDataProfile.ts index b00f89a92b7..782a627770f 100644 --- a/src/vs/platform/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/browser/userDataProfile.ts @@ -31,7 +31,12 @@ export class BrowserUserDataProfilesService extends UserDataProfilesService impl this._register(this.changesBroadcastChannel.onDidReceiveData(changes => { try { this._profilesObject = undefined; - this._onDidChangeProfiles.fire({ added: changes.added.map(p => reviveProfile(p, this.profilesHome.scheme)), removed: changes.removed.map(p => reviveProfile(p, this.profilesHome.scheme)), all: this.profiles }); + this._onDidChangeProfiles.fire({ + added: changes.added.map(p => reviveProfile(p, this.profilesHome.scheme)), + removed: changes.removed.map(p => reviveProfile(p, this.profilesHome.scheme)), + updated: changes.updated.map(p => reviveProfile(p, this.profilesHome.scheme)), + all: this.profiles + }); } catch (error) {/* ignore */ } })); } @@ -54,9 +59,9 @@ export class BrowserUserDataProfilesService extends UserDataProfilesService impl return []; } - protected override triggerProfilesChanges(added: IUserDataProfile[], removed: IUserDataProfile[]) { - super.triggerProfilesChanges(added, removed); - this.changesBroadcastChannel.postData({ added, removed }); + protected override triggerProfilesChanges(added: IUserDataProfile[], removed: IUserDataProfile[], updated: IUserDataProfile[]) { + super.triggerProfilesChanges(added, removed, updated); + this.changesBroadcastChannel.postData({ added, removed, updated }); } protected override saveStoredProfiles(storedProfiles: StoredUserDataProfile[]): void { diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index 3bc2eb4ea3d..3d0c1ba1b4c 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -19,6 +19,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { IStringDictionary } from 'vs/base/common/collections'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { Promises } from 'vs/base/common/async'; +import { generateUuid } from 'vs/base/common/uuid'; /** * Flags to indicate whether to use the default profile or not. @@ -68,7 +69,7 @@ export const PROFILES_ENABLEMENT_CONFIG = 'workbench.experimental.settingsProfil export type EmptyWindowWorkspaceIdentifier = 'empty-window'; export type WorkspaceIdentifier = ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier | EmptyWindowWorkspaceIdentifier; -export type DidChangeProfilesEvent = { readonly added: IUserDataProfile[]; readonly removed: IUserDataProfile[]; readonly all: IUserDataProfile[] }; +export type DidChangeProfilesEvent = { readonly added: IUserDataProfile[]; readonly removed: IUserDataProfile[]; readonly updated: IUserDataProfile[]; readonly all: IUserDataProfile[] }; export type WillCreateProfileEvent = { profile: IUserDataProfile; @@ -91,6 +92,7 @@ export interface IUserDataProfilesService { readonly profiles: IUserDataProfile[]; createProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile>; + updateProfile(profile: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags): Promise<IUserDataProfile>; setProfileForWorkspace(profile: IUserDataProfile, workspaceIdentifier: WorkspaceIdentifier): Promise<void>; getProfile(workspaceIdentifier: WorkspaceIdentifier): IUserDataProfile; removeProfile(profile: IUserDataProfile): Promise<void>; @@ -240,7 +242,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf throw new Error(`Profile with name ${name} already exists`); } - const profile = toUserDataProfile(name, joinPath(this.profilesHome, hash(name).toString(16)), useDefaultFlags); + const profile = toUserDataProfile(name, joinPath(this.profilesHome, hash(generateUuid()).toString(16)), useDefaultFlags); await this.fileService.createFolder(profile.location); const joiners: Promise<void>[] = []; @@ -252,7 +254,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf }); await Promises.settled(joiners); - this.updateProfiles([profile], []); + this.updateProfiles([profile], [], []); if (workspaceIdentifier) { await this.setProfileForWorkspace(profile, workspaceIdentifier); @@ -261,6 +263,22 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf return profile; } + async updateProfile(profileToUpdate: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags): Promise<IUserDataProfile> { + if (!this.enabled) { + throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); + } + + let profile = this.profiles.find(p => p.id === profileToUpdate.id); + if (!profile) { + throw new Error(`Profile '${profileToUpdate.name}' does not exist`); + } + + profile = toUserDataProfile(name, profile.location, useDefaultFlags); + this.updateProfiles([], [], [profile]); + + return profile; + } + async setProfileForWorkspace(profileToSet: IUserDataProfile, workspaceIdentifier: WorkspaceIdentifier): Promise<void> { if (!this.enabled) { throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); @@ -312,7 +330,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } this.updateStoredProfileAssociations(); - this.updateProfiles([], [profile]); + this.updateProfiles([], [profile], []); try { if (this.profiles.length === 1) { @@ -325,24 +343,25 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } } - private updateProfiles(added: IUserDataProfile[], removed: IUserDataProfile[]) { + private updateProfiles(added: IUserDataProfile[], removed: IUserDataProfile[], updated: IUserDataProfile[]) { const storedProfiles: StoredUserDataProfile[] = []; - for (const profile of [...this.profilesObject.profiles, ...added]) { + for (let profile of [...this.profilesObject.profiles, ...added]) { if (profile.isDefault) { continue; } if (removed.some(p => profile.id === p.id)) { continue; } + profile = updated.find(p => profile.id === p.id) ?? profile; storedProfiles.push({ location: profile.location, name: profile.name, useDefaultFlags: profile.useDefaultFlags }); } this.saveStoredProfiles(storedProfiles); this._profilesObject = undefined; - this.triggerProfilesChanges(added, removed); + this.triggerProfilesChanges(added, removed, updated); } - protected triggerProfilesChanges(added: IUserDataProfile[], removed: IUserDataProfile[]) { - this._onDidChangeProfiles.fire({ added, removed, all: this.profiles }); + protected triggerProfilesChanges(added: IUserDataProfile[], removed: IUserDataProfile[], updated: IUserDataProfile[]) { + this._onDidChangeProfiles.fire({ added, removed, updated, all: this.profiles }); } private updateWorkspaceAssociation(workspaceIdentifier: WorkspaceIdentifier, newProfile?: IUserDataProfile) { diff --git a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts index 085e035b676..eb662f36339 100644 --- a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts @@ -41,8 +41,9 @@ export class UserDataProfilesNativeService extends Disposable implements IUserDa this._register(this.channel.listen<DidChangeProfilesEvent>('onDidChangeProfiles')(e => { const added = e.added.map(profile => reviveProfile(profile, this.profilesHome.scheme)); const removed = e.removed.map(profile => reviveProfile(profile, this.profilesHome.scheme)); + const updated = e.updated.map(profile => reviveProfile(profile, this.profilesHome.scheme)); this._profiles = e.all.map(profile => reviveProfile(profile, this.profilesHome.scheme)); - this._onDidChangeProfiles.fire({ added, removed, all: this.profiles }); + this._onDidChangeProfiles.fire({ added, removed, updated, all: this.profiles }); })); } @@ -59,6 +60,11 @@ export class UserDataProfilesNativeService extends Disposable implements IUserDa return this.channel.call('removeProfile', [profile]); } + async updateProfile(profile: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags): Promise<IUserDataProfile> { + const result = await this.channel.call<UriDto<IUserDataProfile>>('updateProfile', [profile, name, useDefaultFlags]); + return reviveProfile(result, this.profilesHome.scheme); + } + getProfile(workspaceIdentifier: WorkspaceIdentifier): IUserDataProfile { throw new Error('Not implemented'); } } diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index a9eb659f467..fce751e1f2d 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, BrowserWindowConstructorOptions, Display, Event, nativeImage, NativeImage, Rectangle, screen, SegmentedControlSegment, systemPreferences, TouchBar, TouchBarSegmentedControl } from 'electron'; +import { app, BrowserWindow, BrowserWindowConstructorOptions, Display, Event, nativeImage, NativeImage, Point, Rectangle, screen, SegmentedControlSegment, systemPreferences, TouchBar, TouchBarSegmentedControl } from 'electron'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -141,6 +141,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { private documentEdited: boolean | undefined; private customTrafficLightPosition: boolean | undefined; + private defaultTrafficLightPosition: Point | undefined; private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[] = []; @@ -291,16 +292,26 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Windows Custom System Context Menu // See https://github.com/electron/electron/issues/24893 + // + // The purpose of this is to allow for the context menu in the Windows Title Bar + // + // Currently, all mouse events in the title bar are captured by the OS + // thus we need to capture them here with a window hook specific to Windows + // and then forward them to the correct window. if (isWindows && useCustomTitleStyle) { - const WM_INITMENU = 0x0116; + const WM_INITMENU = 0x0116; // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-initmenu + + // This sets up a listener for the window hook. This is a Windows-only API provided by electron. this._win.hookWindowMessage(WM_INITMENU, () => { const [x, y] = this._win.getPosition(); const cursorPos = screen.getCursorScreenPoint(); + // This is necessary to make sure the native system context menu does not show up. this._win.setEnabled(false); this._win.setEnabled(true); this._onDidTriggerSystemContextMenu.fire({ x: cursorPos.x - x, y: cursorPos.y - y }); + return 0; // skip native menu }); } @@ -1325,9 +1336,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { const useCustomTrafficLightPosition = this.configurationService.getValue<boolean>(commandCenterSettingKey); if (useCustomTrafficLightPosition) { - this._win.setTrafficLightPosition({ x: 7, y: 9 }); + if (!this.defaultTrafficLightPosition) { + this.defaultTrafficLightPosition = this._win.getTrafficLightPosition(); // remember default to restore later + } + this._win.setTrafficLightPosition({ x: 7, y: 10 }); } else { - this._win.setTrafficLightPosition({ x: 7, y: 6 }); + if (this.defaultTrafficLightPosition) { + this._win.setTrafficLightPosition(this.defaultTrafficLightPosition); + } } this.customTrafficLightPosition = useCustomTrafficLightPosition; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 05bbaab8387..8452c1c5fc4 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -323,7 +323,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // When run with --wait, make sure we keep the paths to wait for if (filesToOpen && openConfig.waitMarkerFileURI) { - filesToOpen.filesToWait = { paths: [...filesToOpen.filesToDiff, filesToOpen.filesToMerge[3] /* [3] is the resulting merge file */, ...filesToOpen.filesToOpenOrCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI }; + filesToOpen.filesToWait = { paths: coalesce([...filesToOpen.filesToDiff, filesToOpen.filesToMerge[3] /* [3] is the resulting merge file */, ...filesToOpen.filesToOpenOrCreate]), waitMarkerFileUri: openConfig.waitMarkerFileURI }; } // These are windows to restore because of hot-exit or from previous session (only performed once on startup!) @@ -477,7 +477,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (filesToOpen && potentialNewWindowsCount === 0) { // Find suitable window or folder path to open files in - const fileToCheck = filesToOpen.filesToOpenOrCreate[0] || filesToOpen.filesToDiff[0] || filesToOpen.filesToMerge[3] /* [3] is the resulting merge file */; + const fileToCheck: IPath<IEditorOptions> | undefined = filesToOpen.filesToOpenOrCreate[0] || filesToOpen.filesToDiff[0] || filesToOpen.filesToMerge[3] /* [3] is the resulting merge file */; // only look at the windows with correct authority const windows = this.getWindows().filter(window => filesToOpen && isEqualAuthority(window.remoteAuthority, filesToOpen.remoteAuthority)); diff --git a/src/vs/server/node/server.cli.ts b/src/vs/server/node/server.cli.ts index 56a704050b3..4c5ec7e1e2a 100644 --- a/src/vs/server/node/server.cli.ts +++ b/src/vs/server/node/server.cli.ts @@ -44,7 +44,7 @@ const isSupportedForCmd = (optionId: keyof RemoteParsedArgs) => { case 'enable-smoke-test-driver': case 'extensions-download-dir': case 'builtin-extensions-dir': - case 'shell-integration': + case 'locate-shell-integration-path': case 'telemetry': return false; default: diff --git a/src/vs/server/node/serverEnvironmentService.ts b/src/vs/server/node/serverEnvironmentService.ts index 2efbb4c5c98..c075cf134fd 100644 --- a/src/vs/server/node/serverEnvironmentService.ts +++ b/src/vs/server/node/serverEnvironmentService.ts @@ -81,7 +81,7 @@ export const serverOptions: OptionDescriptions<ServerParsedArgs> = { 'help': OPTIONS['help'], 'version': OPTIONS['version'], - 'shell-integration': OPTIONS['shell-integration'], + 'locate-shell-integration-path': OPTIONS['locate-shell-integration-path'], 'compatibility': { type: 'string' }, @@ -194,7 +194,7 @@ export interface ServerParsedArgs { /* ----- server cli ----- */ help: boolean; version: boolean; - 'shell-integration'?: string; + 'locate-shell-integration-path'?: string; compatibility: string; diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index bf5de3a19a2..3e537e533af 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -366,45 +366,23 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- copy paste action provider - $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], supportsCopy: boolean, pasteMimeTypes: readonly string[]): void { - const provider: languages.DocumentPasteEditProvider = { - pasteMimeTypes: pasteMimeTypes, + private readonly _pasteEditProviders = new Map<number, MainThreadPasteEditProvider>(); - prepareDocumentPaste: supportsCopy - ? async (model: ITextModel, selections: Selection[], dataTransfer: VSDataTransfer, token: CancellationToken): Promise<VSDataTransfer | undefined> => { - const dataTransferDto = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer); - if (token.isCancellationRequested) { - return undefined; - } - - const result = await this._proxy.$prepareDocumentPaste(handle, model.uri, selections, dataTransferDto, token); - if (!result) { - return undefined; - } - - const dataTransferOut = new VSDataTransfer(); - result.items.forEach(([type, item]) => { - dataTransferOut.replace(type, createStringDataTransferItem(item.asString)); - }); - return dataTransferOut; - } - : undefined, - - provideDocumentPasteEdits: async (model: ITextModel, selections: Selection[], dataTransfer: VSDataTransfer, token: CancellationToken) => { - const d = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer); - const result = await this._proxy.$providePasteEdits(handle, model.uri, selections, d, token); - if (!result) { - return undefined; - } - - return { - insertText: result.insertText, - additionalEdit: result.additionalEdit ? reviveWorkspaceEditDto(result.additionalEdit) : undefined, - }; - } - }; + $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], supportsCopy: boolean, pasteMimeTypes: readonly string[]): void { + const provider = new MainThreadPasteEditProvider(handle, this._proxy, supportsCopy, pasteMimeTypes); + this._pasteEditProviders.set(handle, provider); + this._registrations.set(handle, combinedDisposable( + this._languageFeaturesService.documentPasteEditProvider.register(selector, provider), + toDisposable(() => this._pasteEditProviders.delete(handle)), + )); + } - this._registrations.set(handle, this._languageFeaturesService.documentPasteEditProvider.register(selector, provider)); + $resolvePasteFileData(handle: number, requestId: number, dataIndex: number): Promise<VSBuffer> { + const provider = this._pasteEditProviders.get(handle); + if (!provider) { + throw new Error('Could not find provider'); + } + return provider.resolveFileData(requestId, dataIndex); } // --- formatting @@ -924,6 +902,66 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } } +class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider { + + private readonly dataTransfers = new DataTransferCache(); + + public readonly pasteMimeTypes: readonly string[]; + + readonly prepareDocumentPaste?: (model: ITextModel, ranges: readonly IRange[], dataTransfer: VSDataTransfer, token: CancellationToken) => Promise<undefined | VSDataTransfer>; + + constructor( + private readonly handle: number, + private readonly _proxy: ExtHostLanguageFeaturesShape, + supportsCopy: boolean, + pasteMimeTypes: readonly string[], + ) { + this.pasteMimeTypes = pasteMimeTypes; + + if (supportsCopy) { + this.prepareDocumentPaste = async (model: ITextModel, selections: readonly IRange[], dataTransfer: VSDataTransfer, token: CancellationToken): Promise<VSDataTransfer | undefined> => { + const dataTransferDto = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer); + if (token.isCancellationRequested) { + return undefined; + } + + const result = await this._proxy.$prepareDocumentPaste(handle, model.uri, selections, dataTransferDto, token); + if (!result) { + return undefined; + } + + const dataTransferOut = new VSDataTransfer(); + result.items.forEach(([type, item]) => { + dataTransferOut.replace(type, createStringDataTransferItem(item.asString)); + }); + return dataTransferOut; + }; + } + } + + async provideDocumentPasteEdits(model: ITextModel, selections: Selection[], dataTransfer: VSDataTransfer, token: CancellationToken) { + const request = this.dataTransfers.add(dataTransfer); + try { + const d = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer); + const result = await this._proxy.$providePasteEdits(this.handle, request.id, model.uri, selections, d, token); + if (!result) { + return undefined; + } + + return { + insertText: result.insertText, + additionalEdit: result.additionalEdit ? reviveWorkspaceEditDto(result.additionalEdit) : undefined, + }; + } finally { + request.dispose(); + } + } + + resolveFileData(requestId: number, dataIndex: number): Promise<VSBuffer> { + return this.dataTransfers.resolveDropFileData(requestId, dataIndex); + } +} + class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEditProvider { private readonly dataTransfers = new DataTransferCache(); diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index 5737029a6a4..c8dd0761ef1 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -167,6 +167,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc const webview = this._webviewWorkbenchService.createWebview({ id: handle, + providedId: viewType, options: reviveWebviewOptions(initData.panelOptions), contentOptions: reviveWebviewContentOptions(initData.webviewOptions), extension diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts index ef088e5c054..55a20124434 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -89,7 +89,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private onDidClickLink(handle: extHostProtocol.WebviewHandle, link: string): void { const webview = this.getWebview(handle); if (this.isSupportedLink(webview, URI.parse(link))) { - this._openerService.open(link, { fromUserGesture: true, allowContributedOpeners: true, allowCommands: true }); + this._openerService.open(link, { fromUserGesture: true, allowContributedOpeners: true, allowCommands: true, fromWorkspace: true }); } } diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index b94fab104e1..dc9fc1750b9 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -25,7 +25,7 @@ import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files' import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm'; import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane'; -import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -36,6 +36,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { ILogService } from 'vs/platform/log/common/log'; export interface IUserFriendlyViewsContainerDescriptor { id: string; @@ -97,6 +98,8 @@ interface IUserFriendlyViewDescriptor { contextualTitle?: string; visibility?: string; + size?: number; + // From 'remoteViewDescriptor' type group?: string; remoteName?: string | string[]; @@ -159,6 +162,10 @@ const viewDescriptor: IJSONSchema = { localize('vscode.extension.contributes.view.initialState.hidden', "The view will not be shown in the view container, but will be discoverable through the views menu and other view entry points and can be un-hidden by the user."), localize('vscode.extension.contributes.view.initialState.collapsed', "The view will show in the view container, but will be collapsed.") ] + }, + size: { + type: 'number', + description: localize('vscode.extension.contributs.view.size', "The size of the view. Using a number will behave like the css 'flex' property, and the size will set the initial size when the view is first shown. In the side bar, this is the height of the view."), } } }; @@ -256,7 +263,8 @@ class ViewsExtensionHandler implements IWorkbenchContribution { private viewsRegistry: IViewsRegistry; constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILogService private readonly logService: ILogService ) { this.viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry); this.viewsRegistry = Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry); @@ -499,6 +507,16 @@ class ViewsExtensionHandler implements IWorkbenchContribution { return null; } + let weight: number | undefined = undefined; + if (typeof item.size === 'number') { + checkProposedApiEnabled(extension.description, 'contribViewSize'); + if (container.extensionId?.value === extension.description.identifier.value) { + weight = item.size; + } else { + this.logService.warn(`${extension.description.identifier.value} tried to set the view size of ${item.id} but it was ignored because the view container does not belong to it.`); + } + } + const viewDescriptor = <ICustomTreeViewDescriptor>{ type: type, ctorDescriptor: type === ViewType.Tree ? new SyncDescriptor(TreeViewPane) : new SyncDescriptor(WebviewViewPane), @@ -517,7 +535,8 @@ class ViewsExtensionHandler implements IWorkbenchContribution { group: item.group, remoteAuthority: item.remoteName || (<any>item).remoteAuthority, // TODO@roblou - delete after remote extensions are updated hideByDefault: initialVisibility === InitialVisibility.Hidden, - workspace: viewContainer?.id === REMOTE ? true : undefined + workspace: viewContainer?.id === REMOTE ? true : undefined, + weight }; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 2be02ab4797..073d4e11cdf 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -784,7 +784,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I showNotebookDocument(uriOrDocument, options?) { if (URI.isUri(uriOrDocument)) { extHostApiDeprecation.report('window.showNotebookDocument(uri)', extension, - `Please use 'window.openNotebookDocument' and 'window.showTextDocument'`); + `Please use 'workspace.openNotebookDocument' and 'window.showNotebookDocument'`); } return extHostNotebook.showNotebookDocument(uriOrDocument, options); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 16f0fd0ac13..8766b160f4d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -395,6 +395,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerCallHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerTypeHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerDocumentOnDropEditProvider(handle: number, selector: IDocumentFilterDto[]): void; + $resolvePasteFileData(handle: number, requestId: number, dataIndex: number): Promise<VSBuffer>; $resolveDocumentOnDropFileData(handle: number, requestId: number, dataIndex: number): Promise<VSBuffer>; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; } @@ -1724,8 +1725,8 @@ export interface ExtHostLanguageFeaturesShape { $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: languages.CodeActionContext, token: CancellationToken): Promise<ICodeActionListDto | undefined>; $resolveCodeAction(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<IWorkspaceEditDto | undefined>; $releaseCodeActions(handle: number, cacheId: number): void; - $prepareDocumentPaste(handle: number, uri: UriComponents, ranges: IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise<DataTransferDTO | undefined>; - $providePasteEdits(handle: number, uri: UriComponents, ranges: IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise<IPasteEditDto | undefined>; + $prepareDocumentPaste(handle: number, uri: UriComponents, ranges: readonly IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise<DataTransferDTO | undefined>; + $providePasteEdits(handle: number, requestId: number, uri: UriComponents, ranges: IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise<IPasteEditDto | undefined>; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: languages.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>; $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: languages.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>; $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: languages.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>; diff --git a/src/vs/workbench/api/common/extHostExtensionActivator.ts b/src/vs/workbench/api/common/extHostExtensionActivator.ts index a0a16865ab2..38856e517e8 100644 --- a/src/vs/workbench/api/common/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/common/extHostExtensionActivator.ts @@ -28,10 +28,10 @@ export interface IExtensionAPI { } export type ExtensionActivationTimesFragment = { - startup?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - codeLoadingTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - activateCallTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - activateResolvedTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; + startup?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Activation occurred during startup' }; + codeLoadingTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time it took to load the extension\'s code' }; + activateCallTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time it took to call activate' }; + activateResolvedTime?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time it took for async-activation to finish' }; }; export class ExtensionActivationTimes { diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 337929b481d..ac865608116 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -62,14 +62,14 @@ export interface IHostUtils { } type TelemetryActivationEventFragment = { - id: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; - name: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; - extensionVersion: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; - publisherDisplayName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - activationEvents: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - isBuiltin: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - reasonId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; + id: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The identifier of an extension' }; + name: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The name of the extension' }; + extensionVersion: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The version of the extension' }; + publisherDisplayName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The publisher of the extension' }; + activationEvents: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'All activation events of the extension' }; + isBuiltin: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'If the extension is builtin or git installed' }; + reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The activation event' }; + reasonId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The identifier of the activation event' }; }; export abstract class AbstractExtHostExtensionService extends Disposable implements ExtHostExtensionServiceShape { @@ -425,7 +425,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const event = getTelemetryActivationEvent(extensionDescription, reason); type ExtensionActivationTimesClassification = { owner: 'jrieken'; - outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + comment: 'Timestamps for extension activation'; + outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did extension activation succeed or fail' }; } & TelemetryActivationEventFragment & ExtensionActivationTimesFragment; type ExtensionActivationTimesEvent = { @@ -450,6 +451,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const event = getTelemetryActivationEvent(extensionDescription, reason); type ActivatePluginClassification = { owner: 'jrieken'; + comment: 'Data about how/why an extension was activated'; } & TelemetryActivationEventFragment; this._mainThreadTelemetryProxy.$publicLog2<TelemetryActivationEvent, ActivatePluginClassification>('activatePlugin', event); const entryPoint = this._getEntryPoint(extensionDescription); diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 809b7609426..d9244952ab0 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -512,7 +512,7 @@ class DocumentPasteEditProvider { const vscodeRanges = ranges.map(range => typeConvert.Range.to(range)); const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, async (index) => { - return (await this._proxy.$resolveDocumentOnDropFileData(this._handle, requestId, index)).buffer; + return (await this._proxy.$resolvePasteFileData(this._handle, requestId, index)).buffer; }); const edit = await this._provider.provideDocumentPasteEdits(doc, vscodeRanges, dataTransfer, token); @@ -2471,8 +2471,8 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.prepareDocumentPaste(URI.revive(resource), ranges, dataTransfer, token), undefined, token); } - $providePasteEdits(handle: number, resource: UriComponents, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<extHostProtocol.IPasteEditDto | undefined> { - return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.providePasteEdits(0, URI.revive(resource), ranges, dataTransferDto, token), undefined, token); + $providePasteEdits(handle: number, requestId: number, resource: UriComponents, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<extHostProtocol.IPasteEditDto | undefined> { + return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.providePasteEdits(requestId, URI.revive(resource), ranges, dataTransferDto, token), undefined, token); } // --- configuration diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index 46f3e072488..2a176ad3a91 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -250,7 +250,8 @@ class KeytarNodeModuleFactory implements INodeModuleFactory { const ext = this._extensionPaths.findSubstr(parent); type ShimmingKeytarClassification = { owner: 'jrieken'; - extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + comment: 'Know when the keytar-shim was used'; + extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension is question' }; }; this._mainThreadTelemetry.$publicLog2<{ extension: string }, ShimmingKeytarClassification>('shimming.keytar', { extension: ext?.identifier.value ?? 'unknown_extension' }); return this._impl; @@ -348,7 +349,8 @@ class OpenNodeModuleFactory implements INodeModuleFactory { } type ShimmingOpenClassification = { owner: 'jrieken'; - extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + comment: 'Know when the open-shim was used'; + extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension is question' }; }; this._mainThreadTelemetry.$publicLog2<{ extension: string }, ShimmingOpenClassification>('shimming.open', { extension: this._extensionId }); } @@ -359,7 +361,8 @@ class OpenNodeModuleFactory implements INodeModuleFactory { } type ShimmingOpenCallNoForwardClassification = { owner: 'jrieken'; - extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + comment: 'Know when the open-shim was used'; + extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension is question' }; }; this._mainThreadTelemetry.$publicLog2<{ extension: string }, ShimmingOpenCallNoForwardClassification>('shimming.open.call.noForward', { extension: this._extensionId }); } diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index d2127334678..f8d52778b0a 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -134,7 +134,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } } - const command = prepareCommand(shell, args.args, cwdForPrepareCommand, args.env); + const command = prepareCommand(shell, args.args, !!args.argsCanBeInterpretedByShell, cwdForPrepareCommand, args.env); terminal.sendText(command); // Mark terminal as unused when its session ends, see #112055 diff --git a/src/vs/workbench/api/node/extensionHostProcess.ts b/src/vs/workbench/api/node/extensionHostProcess.ts index bad55575c87..f19fd73d9e2 100644 --- a/src/vs/workbench/api/node/extensionHostProcess.ts +++ b/src/vs/workbench/api/node/extensionHostProcess.ts @@ -286,37 +286,39 @@ function connectToRenderer(protocol: IMessagePassingProtocol): Promise<IRenderer } } - // Kill oneself if one's parent dies. Much drama. - let epermErrors = 0; - setInterval(function () { - try { - process.kill(initData.parentPid, 0); // throws an exception if the main process doesn't exist anymore. - epermErrors = 0; - } catch (e) { - if (e && e.code === 'EPERM') { - // Even if the parent process is still alive, - // some antivirus software can lead to an EPERM error to be thrown here. - // Let's terminate only if we get 3 consecutive EPERM errors. - epermErrors++; - if (epermErrors >= 3) { - onTerminate(`parent process ${initData.parentPid} does not exist anymore (3 x EPERM): ${e.message} (code: ${e.code}) (errno: ${e.errno})`); + if (initData.parentPid) { + // Kill oneself if one's parent dies. Much drama. + let epermErrors = 0; + setInterval(function () { + try { + process.kill(initData.parentPid, 0); // throws an exception if the main process doesn't exist anymore. + epermErrors = 0; + } catch (e) { + if (e && e.code === 'EPERM') { + // Even if the parent process is still alive, + // some antivirus software can lead to an EPERM error to be thrown here. + // Let's terminate only if we get 3 consecutive EPERM errors. + epermErrors++; + if (epermErrors >= 3) { + onTerminate(`parent process ${initData.parentPid} does not exist anymore (3 x EPERM): ${e.message} (code: ${e.code}) (errno: ${e.errno})`); + } + } else { + onTerminate(`parent process ${initData.parentPid} does not exist anymore: ${e.message} (code: ${e.code}) (errno: ${e.errno})`); } - } else { - onTerminate(`parent process ${initData.parentPid} does not exist anymore: ${e.message} (code: ${e.code}) (errno: ${e.errno})`); } + }, 1000); + + // In certain cases, the event loop can become busy and never yield + // e.g. while-true or process.nextTick endless loops + // So also use the native node module to do it from a separate thread + let watchdog: typeof nativeWatchdog; + try { + watchdog = require.__$__nodeRequire('native-watchdog'); + watchdog.start(initData.parentPid); + } catch (err) { + // no problem... + onUnexpectedError(err); } - }, 1000); - - // In certain cases, the event loop can become busy and never yield - // e.g. while-true or process.nextTick endless loops - // So also use the native node module to do it from a separate thread - let watchdog: typeof nativeWatchdog; - try { - watchdog = require.__$__nodeRequire('native-watchdog'); - watchdog.start(initData.parentPid); - } catch (err) { - // no problem... - onUnexpectedError(err); } // Tell the outside that we are initialized diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 3890ccfb5ba..150c192a1b8 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -583,9 +583,6 @@ if (isWindows || isLinux || isWeb) { id: MenuId.MenubarAppearanceMenu, group: '2_workbench_layout', order: 0 - }, { - id: MenuId.TitleBarContext, - order: 0 }] }); } @@ -594,6 +591,16 @@ if (isWindows || isLinux || isWeb) { return accessor.get(IWorkbenchLayoutService).toggleMenuBar(); } }); + + // Add separately to title bar context menu so we can use a different title + MenuRegistry.appendMenuItem(MenuId.TitleBarContext, { + command: { + id: 'workbench.action.toggleMenuBar', + title: localize('miShowMenuBarNoMnemonic', "Show Menu Bar"), + toggled: ContextKeyExpr.and(IsMacNativeContext.toNegated(), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'hidden'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'toggle'), ContextKeyExpr.notEquals('config.window.menuBarVisibility', 'compact')) + }, + order: 0 + }); } // --- Reset View Locations diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 2e997f59f0b..d5f339775a3 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -245,10 +245,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Restore editor part on any editor change this._register(this.editorService.onDidVisibleEditorsChange(showEditorIfHidden)); this._register(this.editorGroupService.onDidActivateGroup(showEditorIfHidden)); - }); - // Revalidate center layout when active editor changes: diff editor quits centered mode. - this._register(this.editorService.onDidActiveEditorChange(() => this.centerEditorLayout(this.stateModel.getRuntimeValue(LayoutStateKeys.EDITOR_CENTERED)))); + // Revalidate center layout when active editor changes: diff editor quits centered mode. + this._register(this.editorService.onDidActiveEditorChange(() => this.centerEditorLayout(this.stateModel.getRuntimeValue(LayoutStateKeys.EDITOR_CENTERED)))); + }); // Configuration changes this._register(this.configurationService.onDidChangeConfiguration(() => this.doUpdateLayoutConfiguration())); @@ -347,7 +347,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.updateMenubarVisibility(!!skipLayout); // Centered Layout - this.centerEditorLayout(this.stateModel.getRuntimeValue(LayoutStateKeys.EDITOR_CENTERED), skipLayout); + this.editorGroupService.whenRestored.then(() => { + this.centerEditorLayout(this.stateModel.getRuntimeValue(LayoutStateKeys.EDITOR_CENTERED), skipLayout); + }); } private setSideBarPosition(position: Position): void { @@ -576,7 +578,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi input2: { resource: filesToMerge[1].resource }, base: { resource: filesToMerge[2].resource }, result: { resource: filesToMerge[3].resource }, - options: { pinned: true, override: 'mergeEditor.Input' } // TODO@bpasero remove the override once the resolver is ready + options: { pinned: true } }]; } diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 3d61bbcb255..b5efad6ca4e 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -235,3 +235,7 @@ body.web { .monaco-workbench .monaco-list:focus { outline: 0 !important; /* tree indicates focus not via outline but through the focused item */ } + +.monaco-workbench a.monaco-link:hover { + text-decoration: underline; /* render underline on hover for accessibility requirements */ +} diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 15973ea64e2..51879dbf0ba 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -26,7 +26,7 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ColorIdentifier, ColorTransform } from 'vs/platform/theme/common/colorRegistry'; import { attachBreadcrumbsStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { DEFAULT_LABELS_CONTAINER, ResourceLabels } from 'vs/workbench/browser/labels'; import { BreadcrumbsConfig, IBreadcrumbsService } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { BreadcrumbsModel, FileElement, OutlineElement2 } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; import { BreadcrumbsFilePicker, BreadcrumbsOutlinePicker, BreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker'; @@ -109,7 +109,7 @@ class FileItem extends BreadcrumbsItem { readonly model: BreadcrumbsModel, readonly element: FileElement, readonly options: IBreadcrumbsControlOptions, - @IInstantiationService private readonly _instantiationService: IInstantiationService + private readonly _labels: ResourceLabels ) { super(); } @@ -130,8 +130,8 @@ class FileItem extends BreadcrumbsItem { render(container: HTMLElement): void { // file/folder - const label = this._instantiationService.createInstance(ResourceLabel, container, {}); - label.element.setFile(this.element.uri, { + const label = this._labels.create(container); + label.setFile(this.element.uri, { hidePath: true, hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons, fileKind: this.element.kind, @@ -182,6 +182,7 @@ export class BreadcrumbsControl { private readonly _disposables = new DisposableStore(); private readonly _breadcrumbsDisposables = new DisposableStore(); + private readonly _labels: ResourceLabels; private _breadcrumbsPickerShowing = false; private _breadcrumbsPickerIgnoreOnceItem: BreadcrumbsItem | undefined; @@ -208,6 +209,8 @@ export class BreadcrumbsControl { this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(configurationService); this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(configurationService); + this._labels = this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER); + const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default'; this._widget = new BreadcrumbsWidget(this.domNode, BreadcrumbsControl.SCROLLBAR_SIZES[sizing], separatorIcon); this._widget.onDidSelectItem(this._onSelectEvent, this, this._disposables); @@ -232,6 +235,7 @@ export class BreadcrumbsControl { this._cfUseQuickPick.dispose(); this._cfShowIcons.dispose(); this._widget.dispose(); + this._labels.dispose(); this.domNode.remove(); } @@ -290,7 +294,7 @@ export class BreadcrumbsControl { showFileIcons: this._options.showFileIcons && showIcons, showSymbolIcons: this._options.showSymbolIcons && showIcons }; - const items = model.getElements().map(element => element instanceof FileElement ? new FileItem(model, element, options, this._instantiationService) : new OutlineItem(model, element, options)); + const items = model.getElements().map(element => element instanceof FileElement ? new FileItem(model, element, options, this._labels) : new OutlineItem(model, element, options)); if (items.length === 0) { this._widget.setEnabled(false); this._widget.setItems([new class extends BreadcrumbsItem { diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index e542f84cff4..67668bd3093 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -17,6 +17,7 @@ import { FileKind } from 'vs/platform/files/common/files'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IOutline, IOutlineService, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; import { IEditorPane } from 'vs/workbench/common/editor'; +import { matchesSomeScheme } from 'vs/platform/opener/common/opener'; export class FileElement { constructor( @@ -116,7 +117,7 @@ export class BreadcrumbsModel { private _initFilePathInfo(uri: URI): FileInfo { - if (uri.scheme === Schemas.untitled) { + if (matchesSomeScheme(uri, Schemas.untitled, Schemas.data)) { return { folder: undefined, path: [] diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index 0464f14265b..d7dc301565a 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -30,6 +30,7 @@ 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'; +import { truncate } from 'vs/base/common/strings'; export interface IEditorPlaceholderContents { icon: string; @@ -48,6 +49,8 @@ export interface IErrorEditorPlaceholderOptions extends IEditorOptions { export abstract class EditorPlaceholder extends EditorPane { + private static readonly PLACEHOLDER_LABEL_MAX_LENGTH = 1024; + private container: HTMLElement | undefined; private scrollbar: DomScrollableElement | undefined; private inputDisposable = this._register(new MutableDisposable()); @@ -96,6 +99,7 @@ export abstract class EditorPlaceholder extends EditorPane { // Delegate to implementation for contents const disposables = new DisposableStore(); const { icon, label, actions } = await this.getContents(input, options, disposables); + const truncatedLabel = truncate(label, EditorPlaceholder.PLACEHOLDER_LABEL_MAX_LENGTH); // Icon const iconContainer = container.appendChild($('.editor-placeholder-icon-container')); @@ -105,11 +109,11 @@ export abstract class EditorPlaceholder extends EditorPane { // Label const labelContainer = container.appendChild($('.editor-placeholder-label-container')); const labelWidget = document.createElement('span'); - labelWidget.textContent = label; + labelWidget.textContent = truncatedLabel; labelContainer.appendChild(labelWidget); // ARIA label - container.setAttribute('aria-label', `${computeEditorAriaLabel(input, undefined, this.group, undefined)}, ${label}`); + container.setAttribute('aria-label', `${computeEditorAriaLabel(input, undefined, this.group, undefined)}, ${truncatedLabel}`); // Actions const actionsContainer = container.appendChild($('.editor-placeholder-actions-container')); diff --git a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css index f3bde77fdca..325e348e2e0 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css +++ b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css @@ -37,6 +37,5 @@ .monaco-editor-pane-placeholder .monaco-link:hover { font-size: 14px; cursor: pointer; - text-decoration: underline; margin-left: 5px; } diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 386e13b2e0b..facf063df26 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -1910,12 +1910,17 @@ registerThemingParticipant((theme, collector) => { const activeContrastBorderColor = theme.getColor(activeContrastBorder); if (activeContrastBorderColor) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active, - .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active:hover { + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active, + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover { outline: 1px solid; outline-offset: -5px; } + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { + outline: 1px dotted; + outline-offset: -5px; + } + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { outline: 1px dashed; outline-offset: -5px; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index d3da50394ad..670e5f35cae 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -86,7 +86,6 @@ export class NotificationsStatus extends Disposable { if (this.notificationService.doNotDisturbMode) { statusProperties = { ...statusProperties, - name: localize('status.doNotDisturb', "Do Not Disturb"), text: `${notificationsInProgress > 0 || this.newNotificationsCount > 0 ? '$(bell-slash-dot)' : '$(bell-slash)'}`, ariaLabel: localize('status.doNotDisturb', "Do Not Disturb"), tooltip: localize('status.doNotDisturbTooltip', "Do Not Disturb Mode is Enabled") diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 8363eed17f6..239a92d2f9a 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -10,7 +10,7 @@ height: 22px; font-size: 12px; display: flex; - overflow: visible; + overflow: hidden; transition: background-color 0.35s ease-out; } @@ -32,8 +32,8 @@ } .monaco-workbench .part.statusbar > .right-items { - flex-direction: row-reverse; - flex-wrap: wrap /* ensures that the most right elements wrap last when space is little */; + flex-wrap: wrap ; /* overflow elements by wrapping */ + flex-direction: row-reverse; /* let the elements to the left wrap first */ } .monaco-workbench .part.statusbar > .left-items { @@ -47,16 +47,21 @@ vertical-align: top; max-width: 40vw; } - .monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak { position: relative; } -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak:before { - content: ''; +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak > .status-bar-item-beak-container { position: absolute; - left: 10px; + left: calc(50% - 5px); /* centering relative to parent */ top: -5px; + width: 10px; + height: 5px; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak > .status-bar-item-beak-container:before { + content: ''; + position: fixed; border-bottom-width: 5px; border-bottom-style: solid; border-left: 5px solid transparent; @@ -83,7 +88,7 @@ } .monaco-workbench .part.statusbar > .items-container > .statusbar-item.right.last-visible-item { - padding-right: 7px; /* Add padding to the most right status bar item */ + margin-right: 7px; /* Add margin to the most right status bar item */ } /* Tweak appearance for items with background to improve hover feedback */ diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts index 6b4685f4901..acad14f1802 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts @@ -19,7 +19,7 @@ import { Command } from 'vs/editor/common/languages'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { renderIcon, renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { syncing } from 'vs/platform/theme/common/iconRegistry'; +import { spinningLoading, syncing } from 'vs/platform/theme/common/iconRegistry'; import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; import { isMarkdownString, markdownStringEqual } from 'vs/base/common/htmlContent'; import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; @@ -41,6 +41,7 @@ export class StatusbarEntryItem extends Disposable { private hover: ICustomHover | undefined = undefined; readonly labelContainer: HTMLElement; + readonly beakContainer: HTMLElement; get name(): string { return assertIsDefined(this.entry).name; @@ -69,17 +70,20 @@ export class StatusbarEntryItem extends Disposable { // Label (with support for progress) this.label = new StatusBarCodiconLabel(this.labelContainer); - - // Add to parent this.container.appendChild(this.labelContainer); + // Beak Container + this.beakContainer = document.createElement('div'); + this.beakContainer.className = 'status-bar-item-beak-container'; + this.container.appendChild(this.beakContainer); + this.update(entry); } update(entry: IStatusbarEntry): void { // Update: Progress - this.label.showProgress = !!entry.showProgress; + this.label.showProgress = entry.showProgress ?? false; // Update: Text if (!this.entry || entry.text !== this.entry.text) { @@ -241,7 +245,7 @@ export class StatusbarEntryItem extends Disposable { class StatusBarCodiconLabel extends SimpleIconLabel { - private readonly progressCodicon = renderIcon(syncing); + private progressCodicon = renderIcon(syncing); private currentText = ''; private currentShowProgress = false; @@ -252,9 +256,10 @@ class StatusBarCodiconLabel extends SimpleIconLabel { super(container); } - set showProgress(showProgress: boolean) { + set showProgress(showProgress: boolean | 'syncing' | 'loading') { if (this.currentShowProgress !== showProgress) { - this.currentShowProgress = showProgress; + this.currentShowProgress = !!showProgress; + this.progressCodicon = renderIcon(showProgress === 'loading' ? spinningLoading : syncing); this.text = this.currentText; } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index ecee307637e..ed738ef9fc9 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -531,6 +531,7 @@ export class StatusbarPart extends Part implements IStatusbarService { } this.styleElement.textContent = ` + /* Status bar focus outline */ .monaco-workbench .part.statusbar:focus { outline-color: ${statusBarFocusColor}; @@ -541,10 +542,11 @@ export class StatusbarPart extends Part implements IStatusbarService { outline: 1px solid ${this.getColor(activeContrastBorder) ?? itemBorderColor}; outline-offset: ${borderColor ? '-2px' : '-1px'}; } + /* Notification Beak */ - .monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak:before { + .monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak > .status-bar-item-beak-container:before { border-bottom-color: ${backgroundColor}; - } + } `; } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 67efd200c19..4f8aa7d5b3a 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -226,7 +226,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IHoverService private readonly hoverService: IHoverService, @IContextKeyService contextKeyService: IContextKeyService, - @IActivityService private readonly activityService: IActivityService + @IActivityService private readonly activityService: IActivityService, + @ILogService private readonly logService: ILogService ) { super(); this.root = new Root(); @@ -799,7 +800,14 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { const tree = this.tree; if (tree && this.visible) { this.refreshing = true; - await Promise.all(elements.map(element => tree.updateChildren(element, true, true))); + try { + await Promise.all(elements.map(element => tree.updateChildren(element, true, true))); + } catch (e) { + // When multiple calls are made to refresh the tree in quick succession, + // we can get a "Tree element not found" error. This is expected. + // Ideally this is fixable, so log instead of ignoring so the error is preserved. + this.logService.error(e); + } this.refreshing = false; this._onDidCompleteRefresh.fire(); this.updateContentAreas(); @@ -1283,9 +1291,10 @@ export class CustomTreeView extends AbstractTreeView { @IHoverService hoverService: IHoverService, @IExtensionService private readonly extensionService: IExtensionService, @IActivityService activityService: IActivityService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @ILogService logService: ILogService, ) { - super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService); + super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService, logService); } protected activate() { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 6f6ab509248..a886ffaee13 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -484,6 +484,8 @@ export interface IResourceDiffEditorInput extends IBaseUntypedEditorInput { readonly modified: IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput; } +export type IResourceMergeEditorInputSide = (IResourceEditorInput | ITextResourceEditorInput) & { detail?: string }; + /** * A resource merge editor input compares multiple editors * highlighting the differences for merging. @@ -496,12 +498,12 @@ export interface IResourceMergeEditorInput extends IBaseUntypedEditorInput { /** * The one changed version of the file. */ - readonly input1: IResourceEditorInput | ITextResourceEditorInput; + readonly input1: IResourceMergeEditorInputSide; /** * The second changed version of the file. */ - readonly input2: IResourceEditorInput | ITextResourceEditorInput; + readonly input2: IResourceMergeEditorInputSide; /** * The base common ancestor of the file to merge. diff --git a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts index bbdbbd99349..dd288894ba7 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint.ts @@ -8,7 +8,7 @@ import { ParseError, parse, getNodeType } from 'vs/base/common/json'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { CharacterPair, CommentRule, EnterAction, ExplicitLanguageConfiguration, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, OnEnterRule } from 'vs/editor/common/languages/languageConfiguration'; +import { CharacterPair, CommentRule, EnterAction, ExplicitLanguageConfiguration, FoldingMarkers, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, OnEnterRule } from 'vs/editor/common/languages/languageConfiguration'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; @@ -45,6 +45,9 @@ interface IOnEnterRule { action: IEnterAction; } +/** + * Serialized form of a language configuration + */ interface ILanguageConfiguration { comments?: CommentRule; brackets?: CharacterPair[]; @@ -53,7 +56,13 @@ interface ILanguageConfiguration { colorizedBracketPairs?: Array<CharacterPair>; wordPattern?: string | IRegExp; indentationRules?: IIndentationRules; - folding?: FoldingRules; + folding?: { + offSide?: boolean; + markers?: { + start?: string | IRegExp; + end?: string | IRegExp; + }; + }; autoCloseBefore?: string; onEnterRules?: IOnEnterRule[]; } @@ -393,10 +402,13 @@ export class LanguageConfigurationFileHandler extends Disposable { const indentationRules = (configuration.indentationRules ? this._mapIndentationRules(languageId, configuration.indentationRules) : undefined); let folding: FoldingRules | undefined = undefined; if (configuration.folding) { - const markers = configuration.folding.markers; + const rawMarkers = configuration.folding.markers; + const startMarker = (rawMarkers && rawMarkers.start ? this._parseRegex(languageId, `folding.markers.start`, rawMarkers.start) : undefined); + const endMarker = (rawMarkers && rawMarkers.end ? this._parseRegex(languageId, `folding.markers.end`, rawMarkers.end) : undefined); + const markers: FoldingMarkers | undefined = (startMarker && endMarker ? { start: startMarker, end: endMarker } : undefined); folding = { offSide: configuration.folding.offSide, - markers: markers ? { start: new RegExp(markers.start), end: new RegExp(markers.end) } : undefined + markers }; } const onEnterRules = this._extractValidOnEnterRules(languageId, configuration); diff --git a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts index bac3e753519..fa456018f48 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts @@ -20,7 +20,7 @@ import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IContentActionHandler, renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; -import { SelectSnippetForEmptyFile } from 'vs/workbench/contrib/snippets/browser/commands/emptyFileSnippets'; +import { ApplyFileSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets'; const $ = dom.$; @@ -108,7 +108,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { this.domNode = $('.untitled-hint'); this.domNode.style.width = 'max-content'; - const hintMsg = localize({ key: 'message', comment: ['Presereve double-square brackets and their order'] }, '[[Select a language]], [[start with a snippet]], or [[open a different editor]] to get started.\nStart typing to dismiss or [[don\'t show]] this again.'); + const hintMsg = localize({ key: 'message', comment: ['Presereve double-square brackets and their order'] }, '[[Select a language]], or [[open a different editor]] to get started.\nStart typing to dismiss or [[don\'t show]] this again.'); const hintHandler: IContentActionHandler = { disposables: this.toDispose, callback: (index, event) => { @@ -117,12 +117,9 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { languageOnClickOrTap(event.browserEvent); break; case '1': - snippetOnClickOrTab(event.browserEvent); - break; - case '2': chooseEditorOnClickOrTap(event.browserEvent); break; - case '3': + case '2': dontShowOnClickOrTap(); break; } @@ -136,7 +133,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { this.domNode.append(hintElement); // ugly way to associate keybindings... - const keybindingsLookup = [ChangeLanguageAction.ID, SelectSnippetForEmptyFile.Id, 'welcome.showNewFileEntries']; + const keybindingsLookup = [ChangeLanguageAction.ID, ApplyFileSnippetAction.Id, 'welcome.showNewFileEntries']; for (const anchor of hintElement.querySelectorAll('A')) { (<HTMLAnchorElement>anchor).style.cursor = 'pointer'; const id = keybindingsLookup.shift(); @@ -153,12 +150,6 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { this.editor.focus(); }; - const snippetOnClickOrTab = async (e: MouseEvent) => { - e.stopPropagation(); - this.editor.focus(); - this.commandService.executeCommand(SelectSnippetForEmptyFile.Id, { from: 'hint' }); - }; - const chooseEditorOnClickOrTap = async (e: MouseEvent) => { e.stopPropagation(); diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 42ebc26458d..3d8c19fce39 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -186,13 +186,11 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> return renderedComment; } - private getIcon(commentCount: number, threadState?: CommentThreadState): Codicon { + private getIcon(threadState?: CommentThreadState): Codicon { if (threadState === CommentThreadState.Unresolved) { return Codicon.commentUnresolved; - } else if (commentCount === 1) { - return Codicon.comment; } else { - return Codicon.commentDiscussion; + return Codicon.comment; } } @@ -200,7 +198,7 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> const commentCount = node.element.replies.length + 1; templateData.threadMetadata.icon.classList.remove(...Array.from(templateData.threadMetadata.icon.classList.values()) .filter(value => value.startsWith('codicon'))); - templateData.threadMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(this.getIcon(commentCount, node.element.threadState))); + templateData.threadMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(this.getIcon(node.element.threadState))); if (node.element.threadState !== undefined) { const color = this.getCommentThreadWidgetStateColor(node.element.threadState, this.themeService.getColorTheme()); templateData.threadMetadata.icon.style.setProperty(commentViewThreadStateColorVar, `${color}`); @@ -219,7 +217,7 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> templateData.disposables.push(disposables); const renderedComment = this.getRenderedComment(originalComment.comment.body, disposables); templateData.disposables.push(renderedComment); - templateData.threadMetadata.commentPreview.appendChild(renderedComment.element); + templateData.threadMetadata.commentPreview.appendChild(renderedComment.element.firstElementChild ?? renderedComment.element); templateData.threadMetadata.commentPreview.title = renderedComment.element.textContent ?? ''; } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index af5bc6b10aa..116c74df092 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -40,6 +40,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { const id = generateUuid(); const webview = accessor.get(IWebviewService).createWebviewOverlay({ id, + providedId: viewType, options: { customClasses: options?.customClasses }, contentOptions: {}, extension: undefined, diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index a1670e79b80..9bc10254662 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -108,14 +108,15 @@ export class CustomEditorInputSerializer extends WebviewEditorInputSerializer { } } -function reviveWebview(webviewService: IWebviewService, data: { id: string; origin: string | undefined; state: any; webviewOptions: WebviewOptions; contentOptions: WebviewContentOptions; extension?: WebviewExtensionDescription }) { +function reviveWebview(webviewService: IWebviewService, data: { id: string; origin: string | undefined; viewType: string; state: any; webviewOptions: WebviewOptions; contentOptions: WebviewContentOptions; extension?: WebviewExtensionDescription }) { const webview = webviewService.createWebviewOverlay({ id: data.id, + providedId: data.viewType, origin: data.origin, options: { purpose: WebviewContentPurpose.CustomEditor, enableFindWidget: data.webviewOptions.enableFindWidget, - retainContextWhenHidden: data.webviewOptions.retainContextWhenHidden + retainContextWhenHidden: data.webviewOptions.retainContextWhenHidden, }, contentOptions: data.contentOptions, extension: data.extension, @@ -187,6 +188,7 @@ export class ComplexCustomWorkingCopyEditorHandler extends Disposable implements const extension = reviveWebviewExtensionDescription(backupData.extension?.id, backupData.extension?.location); const webview = reviveWebview(this._webviewService, { id, + viewType: backupData.viewType, origin: backupData.webview.origin, webviewOptions: restoreWebviewOptions(backupData.webview.options), contentOptions: restoreWebviewContentOptions(backupData.webview.options), diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index db0ce90b3b1..3be7a7f1b16 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -26,7 +26,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { CONTEXT_ACTIVE_CUSTOM_EDITOR_ID, CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CustomEditorCapabilities, CustomEditorInfo, CustomEditorInfoCollection, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorResolverService, IEditorType, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { DiffEditorInputFactoryFunction, EditorInputFactoryFunction, IEditorResolverService, IEditorType, RegisteredEditorPriority, UntitledEditorInputFactoryFunction } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ContributedCustomEditors } from '../common/contributedCustomEditors'; import { CustomEditorInput } from './customEditorInput'; @@ -116,6 +116,17 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ if (!globPattern.filenamePattern) { continue; } + + const editorInputFactory: EditorInputFactoryFunction = ({ resource }, group) => { + return { editor: CustomEditorInput.create(this.instantiationService, resource, contributedEditor.id, group.id) }; + }; + const untitledEditorInputFactory: UntitledEditorInputFactoryFunction = ({ resource }, group) => { + return { editor: CustomEditorInput.create(this.instantiationService, resource ?? URI.from({ scheme: Schemas.untitled, authority: `Untitled-${this._untitledCounter++}` }), contributedEditor.id, group.id) }; + }; + const diffEditorInputFactory: DiffEditorInputFactoryFunction = (diffEditorInput, group) => { + return { editor: this.createDiffEditorInput(diffEditorInput, contributedEditor.id, group) }; + }; + this._editorResolverDisposables.add(this.editorResolverService.registerEditor( globPattern.filenamePattern, { @@ -127,14 +138,10 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ { singlePerResource: () => !this.getCustomEditorCapabilities(contributedEditor.id)?.supportsMultipleEditorsPerDocument ?? true }, - ({ resource }, group) => { - return { editor: CustomEditorInput.create(this.instantiationService, resource, contributedEditor.id, group.id) }; - }, - ({ resource }, group) => { - return { editor: CustomEditorInput.create(this.instantiationService, resource ?? URI.from({ scheme: Schemas.untitled, authority: `Untitled-${this._untitledCounter++}` }), contributedEditor.id, group.id) }; - }, - (diffEditorInput, group) => { - return { editor: this.createDiffEditorInput(diffEditorInput, contributedEditor.id, group) }; + { + createEditorInput: editorInputFactory, + createUntitledEditorInput: untitledEditorInputFactory, + createDiffEditorInput: diffEditorInputFactory, } )); } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 54d0f0a0e7c..ebedc061c54 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -20,7 +20,7 @@ import { } from 'vs/workbench/contrib/debug/common/debug'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { DebugService } from 'vs/workbench/contrib/debug/browser/debugService'; -import { ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_RUN_COMMAND_ID, EDIT_EXPRESSION_COMMAND_ID, REMOVE_EXPRESSION_COMMAND_ID, SELECT_AND_START_ID, SELECT_AND_START_LABEL, SET_EXPRESSION_COMMAND_ID, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, NEXT_DEBUG_CONSOLE_ID, NEXT_DEBUG_CONSOLE_LABEL, PREV_DEBUG_CONSOLE_ID, PREV_DEBUG_CONSOLE_LABEL, OPEN_LOADED_SCRIPTS_LABEL, SHOW_LOADED_SCRIPTS_ID, DEBUG_QUICK_ACCESS_PREFIX, DEBUG_CONSOLE_QUICK_ACCESS_PREFIX, SELECT_DEBUG_CONSOLE_ID, SELECT_DEBUG_CONSOLE_LABEL, STEP_INTO_TARGET_LABEL, STEP_INTO_TARGET_ID, CALLSTACK_TOP_ID, CALLSTACK_TOP_LABEL, CALLSTACK_BOTTOM_LABEL, CALLSTACK_UP_LABEL, CALLSTACK_BOTTOM_ID, CALLSTACK_UP_ID, CALLSTACK_DOWN_ID, CALLSTACK_DOWN_LABEL, DEBUG_COMMAND_CATEGORY } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import { ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_RUN_COMMAND_ID, EDIT_EXPRESSION_COMMAND_ID, REMOVE_EXPRESSION_COMMAND_ID, SELECT_AND_START_ID, SELECT_AND_START_LABEL, SET_EXPRESSION_COMMAND_ID, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, NEXT_DEBUG_CONSOLE_ID, NEXT_DEBUG_CONSOLE_LABEL, PREV_DEBUG_CONSOLE_ID, PREV_DEBUG_CONSOLE_LABEL, OPEN_LOADED_SCRIPTS_LABEL, SHOW_LOADED_SCRIPTS_ID, DEBUG_QUICK_ACCESS_PREFIX, DEBUG_CONSOLE_QUICK_ACCESS_PREFIX, SELECT_DEBUG_CONSOLE_ID, SELECT_DEBUG_CONSOLE_LABEL, STEP_INTO_TARGET_LABEL, STEP_INTO_TARGET_ID, CALLSTACK_TOP_ID, CALLSTACK_TOP_LABEL, CALLSTACK_BOTTOM_LABEL, CALLSTACK_UP_LABEL, CALLSTACK_BOTTOM_ID, CALLSTACK_UP_ID, CALLSTACK_DOWN_ID, CALLSTACK_DOWN_LABEL, DEBUG_COMMAND_CATEGORY, SELECT_DEBUG_SESSION_ID, SELECT_DEBUG_SESSION_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { StatusBarColorProvider } from 'vs/workbench/contrib/debug/browser/statusbarColorProvider'; import { IViewsRegistry, Extensions as ViewExtensions, IViewContainersRegistry, ViewContainerLocation, ViewContainer } from 'vs/workbench/common/views'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; @@ -93,7 +93,6 @@ Registry.as<IQuickAccessRegistry>(QuickAccessExtensions.Quickaccess).registerQui helpEntries: [{ description: nls.localize('tasksQuickAccessHelp', "Show All Debug Consoles"), commandId: SELECT_DEBUG_CONSOLE_ID }] }); - registerEditorContribution('editor.contrib.callStack', CallStackEditorContribution); registerEditorContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointEditorContribution); registerEditorContribution(EDITOR_CONTRIBUTION_ID, DebugEditorContribution); @@ -136,6 +135,7 @@ registerDebugCommandPaletteItem(NEXT_DEBUG_CONSOLE_ID, NEXT_DEBUG_CONSOLE_LABEL) registerDebugCommandPaletteItem(PREV_DEBUG_CONSOLE_ID, PREV_DEBUG_CONSOLE_LABEL); registerDebugCommandPaletteItem(SHOW_LOADED_SCRIPTS_ID, OPEN_LOADED_SCRIPTS_LABEL, CONTEXT_IN_DEBUG_MODE); registerDebugCommandPaletteItem(SELECT_DEBUG_CONSOLE_ID, SELECT_DEBUG_CONSOLE_LABEL); +registerDebugCommandPaletteItem(SELECT_DEBUG_SESSION_ID, SELECT_DEBUG_SESSION_LABEL); registerDebugCommandPaletteItem(CALLSTACK_TOP_ID, CALLSTACK_TOP_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugCommandPaletteItem(CALLSTACK_BOTTOM_ID, CALLSTACK_BOTTOM_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugCommandPaletteItem(CALLSTACK_UP_ID, CALLSTACK_UP_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 92b3b970f07..425e5c70f68 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -32,6 +32,7 @@ import { isWeb, isWindows } from 'vs/base/common/platform'; import { saveAllBeforeDebugStart } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { showLoadedScriptMenu } from 'vs/workbench/contrib/debug/common/loadedScriptsPicker'; +import { showDebugSessionMenu } from 'vs/workbench/contrib/debug/browser/debugSessionPicker'; export const ADD_CONFIGURATION_ID = 'debug.addConfiguration'; export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint'; @@ -55,6 +56,7 @@ export const JUMP_TO_CURSOR_ID = 'debug.jumpToCursor'; export const FOCUS_SESSION_ID = 'workbench.action.debug.focusProcess'; export const SELECT_AND_START_ID = 'workbench.action.debug.selectandstart'; export const SELECT_DEBUG_CONSOLE_ID = 'workbench.action.debug.selectDebugConsole'; +export const SELECT_DEBUG_SESSION_ID = 'workbench.action.debug.selectDebugSession'; export const DEBUG_CONFIGURE_COMMAND_ID = 'workbench.action.debug.configure'; export const DEBUG_START_COMMAND_ID = 'workbench.action.debug.start'; export const DEBUG_RUN_COMMAND_ID = 'workbench.action.debug.run'; @@ -94,6 +96,7 @@ export const CALLSTACK_UP_LABEL = { value: nls.localize('callStackUp', "Navigate export const CALLSTACK_DOWN_LABEL = { value: nls.localize('callStackDown', "Navigate Down Call Stack"), original: 'Navigate Down Call Stack' }; export const SELECT_DEBUG_CONSOLE_LABEL = { value: nls.localize('selectDebugConsole', "Select Debug Console"), original: 'Select Debug Console' }; +export const SELECT_DEBUG_SESSION_LABEL = { value: nls.localize('selectDebugSession', "Select Debug Session"), original: 'Select Debug Session' }; export const DEBUG_QUICK_ACCESS_PREFIX = 'debug '; export const DEBUG_CONSOLE_QUICK_ACCESS_PREFIX = 'debug consoles '; @@ -710,6 +713,13 @@ CommandsRegistry.registerCommand({ } }); +CommandsRegistry.registerCommand({ + id: SELECT_DEBUG_SESSION_ID, + handler: async (accessor: ServicesAccessor) => { + showDebugSessionMenu(accessor, SELECT_AND_START_ID); + } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: DEBUG_START_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 29a568d478b..99db6b2a7a1 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -296,7 +296,8 @@ export class DebugSession implements IDebugSession { locale: platform.locale, supportsProgressReporting: true, // #92253 supportsInvalidatedEvent: true, // #106745 - supportsMemoryReferences: true //#129684 + supportsMemoryReferences: true, //#129684 + supportsArgsCanBeInterpretedByShell: true // #149910 }); this.initialized = true; diff --git a/src/vs/workbench/contrib/debug/browser/debugSessionPicker.ts b/src/vs/workbench/contrib/debug/browser/debugSessionPicker.ts new file mode 100644 index 00000000000..98dfb2e82f5 --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugSessionPicker.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; +import { matchesFuzzy } from 'vs/base/common/filters'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IDebugService, IDebugSession, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; + +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IPickerDebugItem } from 'vs/workbench/contrib/debug/common/loadedScriptsPicker'; +import { IViewsService } from 'vs/workbench/common/views'; +import { ICommandService } from 'vs/platform/commands/common/commands'; + + +export async function showDebugSessionMenu(accessor: ServicesAccessor, selectAndStartID: string) { + const quickInputService = accessor.get(IQuickInputService); + const debugService = accessor.get(IDebugService); + const viewsService = accessor.get(IViewsService); + const commandService = accessor.get(ICommandService); + + const localDisposableStore = new DisposableStore(); + const quickPick = quickInputService.createQuickPick<IPickerDebugItem>(); + localDisposableStore.add(quickPick); + quickPick.matchOnLabel = quickPick.matchOnDescription = quickPick.matchOnDetail = quickPick.sortByLabel = false; + quickPick.placeholder = nls.localize('moveFocusedView.selectView', 'Search for debug session by name'); + quickPick.items = _getPicks(quickPick.value, selectAndStartID, debugService, viewsService, commandService); + + localDisposableStore.add(quickPick.onDidChangeValue(async () => { + quickPick.items = _getPicks(quickPick.value, selectAndStartID, debugService, viewsService, commandService); + })); + localDisposableStore.add(quickPick.onDidAccept(() => { + const selectedItem = quickPick.selectedItems[0]; + selectedItem.accept(); + quickPick.hide(); + localDisposableStore.dispose(); + })); + quickPick.show(); +} + +function _getPicks(filter: string, selectAndStartID: string, debugService: IDebugService, viewsService: IViewsService, commandService: ICommandService): Array<IPickerDebugItem | IQuickPickSeparator> { + const debugConsolePicks: Array<IPickerDebugItem | IQuickPickSeparator> = []; + const headerSessions: IDebugSession[] = []; + + const sessions = debugService.getModel().getSessions(false); + + sessions.forEach((session) => { + if (session.compact && session.parentSession) { + headerSessions.push(session.parentSession); + } + }); + + sessions.forEach((session) => { + const isHeader = headerSessions.includes(session); + if (!session.parentSession) { + debugConsolePicks.push({ type: 'separator', label: isHeader ? session.name : undefined }); + } + + if (!isHeader) { + const pick = _createPick(session, filter, debugService, viewsService, commandService); + if (pick) { + debugConsolePicks.push(pick); + } + } + }); + + if (debugConsolePicks.length) { + debugConsolePicks.push({ type: 'separator' }); + } + + const createDebugSessionLabel = nls.localize('workbench.action.debug.startDebug', 'Start a New Debug Session'); + debugConsolePicks.push({ + label: `$(plus) ${createDebugSessionLabel}`, + ariaLabel: createDebugSessionLabel, + accept: () => commandService.executeCommand(selectAndStartID) + }); + + return debugConsolePicks; +} + + +function _getSessionInfo(session: IDebugSession): { label: string; description: string; ariaLabel: string } { + const label = (!session.configuration.name.length) ? session.name : session.configuration.name; + const parentName = session.compact ? undefined : session.parentSession?.configuration.name; + let description = ''; + let ariaLabel = ''; + if (parentName) { + ariaLabel = nls.localize('workbench.action.debug.spawnFrom', 'Session {0} spawned from {1}', label, parentName); + description = parentName; + } + + return { label, description, ariaLabel }; +} + +function _createPick(session: IDebugSession, filter: string, debugService: IDebugService, viewsService: IViewsService, commandService: ICommandService): IPickerDebugItem | undefined { + const pickInfo = _getSessionInfo(session); + const highlights = matchesFuzzy(filter, pickInfo.label, true); + if (highlights) { + return { + label: pickInfo.label, + description: pickInfo.description, + ariaLabel: pickInfo.ariaLabel, + highlights: { label: highlights }, + accept: () => { + debugService.focusStackFrame(undefined, undefined, session, { explicit: true }); + if (!viewsService.isViewVisible(REPL_VIEW_ID)) { + viewsService.openView(REPL_VIEW_ID, true); + } + } + }; + } + return undefined; +} + + diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index b5a858f7e52..3e4b703689a 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -10,7 +10,7 @@ declare module DebugProtocol { /** Base class of requests, responses, and events. */ interface ProtocolMessage { - /** Sequence number (also known as message ID). For protocol messages of type 'request' this ID can be used to cancel the request. */ + /** Sequence number of the message (also known as message ID). The `seq` for the first message sent by a client or debug adapter is 1, and for each subsequent message is 1 greater than the previous message sent by that actor. `seq` can be used to order requests, responses, and events, and to associate requests with their corresponding responses. For protocol messages of type `request` the sequence number can be used to cancel the request. */ seq: number; /** Message type. Values: 'request', 'response', 'event', etc. @@ -42,13 +42,13 @@ declare module DebugProtocol { /** Sequence number of the corresponding request. */ request_seq: number; /** Outcome of the request. - If true, the request was successful and the 'body' attribute may contain the result of the request. - If the value is false, the attribute 'message' contains the error in short form and the 'body' may contain additional information (see 'ErrorResponse.body.error'). + If true, the request was successful and the `body` attribute may contain the result of the request. + If the value is false, the attribute `message` contains the error in short form and the `body` may contain additional information (see `ErrorResponse.body.error`). */ success: boolean; /** The command requested. */ command: string; - /** Contains the raw error in short form if 'success' is false. + /** Contains the raw error in short form if `success` is false. This raw error might be interpreted by the client and is not shown in the UI. Some predefined values exist. Values: @@ -60,7 +60,7 @@ declare module DebugProtocol { body?: any; } - /** On error (whenever 'success' is false), the body can provide more details. */ + /** On error (whenever `success` is false), the body can provide more details. */ interface ErrorResponse extends Response { body: { /** An optional, structured error message. */ @@ -69,48 +69,47 @@ declare module DebugProtocol { } /** Cancel request; value of command field is 'cancel'. - The 'cancel' request is used by the client in two situations: + The `cancel` request is used by the client in two situations: - to indicate that it is no longer interested in the result produced by a specific request issued earlier - - to cancel a progress sequence. Clients should only call this request if the capability 'supportsCancelRequest' is true. + - to cancel a progress sequence. Clients should only call this request if the corresponding capability `supportsCancelRequest` is true. This request has a hint characteristic: a debug adapter can only be expected to make a 'best effort' in honouring this request but there are no guarantees. - The 'cancel' request may return an error if it could not cancel an operation but a client should refrain from presenting this error to end users. - A client should only call this request if the capability 'supportsCancelRequest' is true. - The request that got cancelled still needs to send a response back. This can either be a normal result ('success' attribute true) or an error response ('success' attribute false and the 'message' set to 'cancelled'). + The `cancel` request may return an error if it could not cancel an operation but a client should refrain from presenting this error to end users. + The request that got cancelled still needs to send a response back. This can either be a normal result (`success` attribute true) or an error response (`success` attribute false and the `message` set to `cancelled`). Returning partial results from a cancelled request is possible but please note that a client has no generic way for detecting that a response is partial or not. - The progress that got cancelled still needs to send a 'progressEnd' event back. - A client should not assume that progress just got cancelled after sending the 'cancel' request. + The progress that got cancelled still needs to send a `progressEnd` event back. + A client should not assume that progress just got cancelled after sending the `cancel` request. */ interface CancelRequest extends Request { // command: 'cancel'; arguments?: CancelArguments; } - /** Arguments for 'cancel' request. */ + /** Arguments for `cancel` request. */ interface CancelArguments { - /** The ID (attribute 'seq') of the request to cancel. If missing no request is cancelled. - Both a 'requestId' and a 'progressId' can be specified in one request. + /** The ID (attribute `seq`) of the request to cancel. If missing no request is cancelled. + Both a `requestId` and a `progressId` can be specified in one request. */ requestId?: number; - /** The ID (attribute 'progressId') of the progress to cancel. If missing no progress is cancelled. - Both a 'requestId' and a 'progressId' can be specified in one request. + /** The ID (attribute `progressId`) of the progress to cancel. If missing no progress is cancelled. + Both a `requestId` and a `progressId` can be specified in one request. */ progressId?: string; } - /** Response to 'cancel' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `cancel` request. This is just an acknowledgement, so no body field is required. */ interface CancelResponse extends Response { } /** Event message for 'initialized' event type. This event indicates that the debug adapter is ready to accept configuration requests (e.g. SetBreakpointsRequest, SetExceptionBreakpointsRequest). - A debug adapter is expected to send this event when it is ready to accept configuration requests (but not before the 'initialize' request has finished). + A debug adapter is expected to send this event when it is ready to accept configuration requests (but not before the `initialize` request has finished). The sequence of events/requests is as follows: - - adapters sends 'initialized' event (after the 'initialize' request has returned) - - client sends zero or more 'setBreakpoints' requests - - client sends one 'setFunctionBreakpoints' request (if capability 'supportsFunctionBreakpoints' is true) - - client sends a 'setExceptionBreakpoints' request if one or more 'exceptionBreakpointFilters' have been defined (or if 'supportsConfigurationDoneRequest' is not true) + - adapters sends `initialized` event (after the `initialize` request has returned) + - client sends zero or more `setBreakpoints` requests + - client sends one `setFunctionBreakpoints` request (if corresponding capability `supportsFunctionBreakpoints` is true) + - client sends a `setExceptionBreakpoints` request if one or more `exceptionBreakpointFilters` have been defined (or if `supportsConfigurationDoneRequest` is not true) - client sends other future configuration requests - - client sends one 'configurationDone' request to indicate the end of the configuration. + - client sends one `configurationDone` request to indicate the end of the configuration. */ interface InitializedEvent extends Event { // event: 'initialized'; @@ -124,7 +123,7 @@ declare module DebugProtocol { // event: 'stopped'; body: { /** The reason for the event. - For backward compatibility this string is shown in the UI if the 'description' attribute is missing (but it must not be translated). + For backward compatibility this string is shown in the UI if the `description` attribute is missing (but it must not be translated). Values: 'step', 'breakpoint', 'exception', 'pause', 'entry', 'goto', 'function breakpoint', 'data breakpoint', 'instruction breakpoint', etc. */ reason: 'step' | 'breakpoint' | 'exception' | 'pause' | 'entry' | 'goto' | 'function breakpoint' | 'data breakpoint' | 'instruction breakpoint' | string; @@ -134,14 +133,14 @@ declare module DebugProtocol { threadId?: number; /** A value of true hints to the client that this event should not change the focus. */ preserveFocusHint?: boolean; - /** Additional information. E.g. if reason is 'exception', text contains the exception name. This string is shown in the UI. */ + /** Additional information. E.g. if reason is `exception`, text contains the exception name. This string is shown in the UI. */ text?: string; - /** If 'allThreadsStopped' is true, a debug adapter can announce that all threads have stopped. + /** If `allThreadsStopped` is true, a debug adapter can announce that all threads have stopped. - The client should use this information to enable that all threads can be expanded to access their stacktraces. - If the attribute is missing or false, only the thread with the given threadId can be expanded. */ allThreadsStopped?: boolean; - /** Ids of the breakpoints that triggered the event. In most cases there will be only a single breakpoint but here are some examples for multiple breakpoints: + /** Ids of the breakpoints that triggered the event. In most cases there is only a single breakpoint but here are some examples for multiple breakpoints: - Different types of breakpoints map to the same location. - Multiple source breakpoints get collapsed to the same instruction by the compiler/runtime. - Multiple function breakpoints with different function names map to the same location. @@ -152,15 +151,15 @@ declare module DebugProtocol { /** Event message for 'continued' event type. The event indicates that the execution of the debuggee has continued. - Please note: a debug adapter is not expected to send this event in response to a request that implies that execution continues, e.g. 'launch' or 'continue'. - It is only necessary to send a 'continued' event if there was no previous request that implied this. + Please note: a debug adapter is not expected to send this event in response to a request that implies that execution continues, e.g. `launch` or `continue`. + It is only necessary to send a `continued` event if there was no previous request that implied this. */ interface ContinuedEvent extends Event { // event: 'continued'; body: { /** The thread which was continued. */ threadId: number; - /** If 'allThreadsContinued' is true, a debug adapter can announce that all threads have continued. */ + /** If `allThreadsContinued` is true, a debug adapter can announce that all threads have continued. */ allThreadsContinued?: boolean; }; } @@ -182,8 +181,8 @@ declare module DebugProtocol { interface TerminatedEvent extends Event { // event: 'terminated'; body?: { - /** A debug adapter may set 'restart' to true (or to an arbitrary object) to request that the front end restarts the session. - The value is not interpreted by the client and passed unmodified as an attribute '__restart' to the 'launch' and 'attach' requests. + /** A debug adapter may set `restart` to true (or to an arbitrary object) to request that the front end restarts the session. + The value is not interpreted by the client and passed unmodified as an attribute `__restart` to the `launch` and `attach` requests. */ restart?: any; }; @@ -210,10 +209,10 @@ declare module DebugProtocol { interface OutputEvent extends Event { // event: 'output'; body: { - /** The output category. If not specified or if the category is not understood by the client, 'console' is assumed. + /** The output category. If not specified or if the category is not understood by the client, `console` is assumed. Values: 'console': Show the output in the client's default message UI, e.g. a 'debug console'. This category should only be used for informational output from the debugger (as opposed to the debuggee). - 'important': A hint for the client to show the output in the client's UI for important and highly visible information, e.g. as a popup notification. This category should only be used for important messages from the debugger (as opposed to the debuggee). Since this category value is a hint, clients might ignore the hint and assume the 'console' category. + 'important': A hint for the client to show the output in the client's UI for important and highly visible information, e.g. as a popup notification. This category should only be used for important messages from the debugger (as opposed to the debuggee). Since this category value is a hint, clients might ignore the hint and assume the `console` category. 'stdout': Show the output as normal program output from the debuggee. 'stderr': Show the output as error program output from the debuggee. 'telemetry': Send the output to telemetry instead of showing it to the user. @@ -224,14 +223,14 @@ declare module DebugProtocol { output: string; /** Support for keeping an output log organized by grouping related messages. 'start': Start a new group in expanded mode. Subsequent output events are members of the group and should be shown indented. - The 'output' attribute becomes the name of the group and is not indented. + The `output` attribute becomes the name of the group and is not indented. 'startCollapsed': Start a new group in collapsed mode. Subsequent output events are members of the group and should be shown indented (as soon as the group is expanded). - The 'output' attribute becomes the name of the group and is not indented. + The `output` attribute becomes the name of the group and is not indented. 'end': End the current group and decrease the indentation of subsequent output events. - A nonempty 'output' attribute is shown as the unindented end of the group. + A nonempty `output` attribute is shown as the unindented end of the group. */ group?: 'start' | 'startCollapsed' | 'end'; - /** If an attribute 'variablesReference' exists and its value is > 0, the output contains objects which can be retrieved by passing 'variablesReference' to the 'variables' request. The value should be less than or equal to 2147483647 (2^31-1). */ + /** If an attribute `variablesReference` exists and its value is > 0, the output contains objects which can be retrieved by passing `variablesReference` to the `variables` request. The value should be less than or equal to 2147483647 (2^31-1). */ variablesReference?: number; /** An optional source location where the output was produced. */ source?: Source; @@ -239,7 +238,7 @@ declare module DebugProtocol { line?: number; /** An optional source location column where the output was produced. */ column?: number; - /** Optional data to report. For the 'telemetry' category the data will be sent to telemetry, for the other categories the data is shown in JSON format. */ + /** Optional data to report. For the `telemetry` category the data is sent to telemetry, for the other categories the data is shown in JSON format. */ data?: any; }; } @@ -254,7 +253,7 @@ declare module DebugProtocol { Values: 'changed', 'new', 'removed', etc. */ reason: 'changed' | 'new' | 'removed' | string; - /** The 'id' attribute is used to find the target breakpoint, the other attributes are used as the new values. */ + /** The `id` attribute is used to find the target breakpoint, the other attributes are used as the new values. */ breakpoint: Breakpoint; }; } @@ -267,7 +266,7 @@ declare module DebugProtocol { body: { /** The reason for the event. */ reason: 'new' | 'changed' | 'removed'; - /** The new, changed, or removed module. In case of 'removed' only the module id is used. */ + /** The new, changed, or removed module. In case of `removed` only the module id is used. */ module: Module; }; } @@ -293,7 +292,7 @@ declare module DebugProtocol { body: { /** The logical name of the process. This is usually the full path to process's executable file. Example: /home/example/myproj/program.js. */ name: string; - /** The system process id of the debugged process. This property will be missing for non-system processes. */ + /** The system process id of the debugged process. This property is missing for non-system processes. */ systemProcessId?: number; /** If true, the process is running on the same computer as the debug adapter. */ isLocalProcess?: boolean; @@ -325,12 +324,12 @@ declare module DebugProtocol { /** Event message for 'progressStart' event type. The event signals that a long running operation is about to start and provides additional information for the client to set up a corresponding progress and cancellation UI. The client is free to delay the showing of the UI in order to reduce flicker. - This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. + This event should only be sent if the corresponding capability `supportsProgressReporting` is true */ interface ProgressStartEvent extends Event { // event: 'progressStart'; body: { - /** An ID that must be used in subsequent 'progressUpdate' and 'progressEnd' events to make them refer to the same progress reporting. + /** An ID that must be used in subsequent `progressUpdate` and `progressEnd` events to make them refer to the same progress reporting. IDs must be unique within a debug session. */ progressId: string; @@ -340,14 +339,14 @@ declare module DebugProtocol { If the request ID is omitted, the progress report is assumed to be related to some general activity of the debug adapter. */ requestId?: number; - /** If true, the request that reports progress may be cancelled with a 'cancel' request. + /** If true, the request that reports progress may be cancelled with a `cancel` request. So this property basically controls whether the client should use UX that supports cancellation. Clients that don't support cancellation are allowed to ignore the setting. */ cancellable?: boolean; /** Optional, more detailed progress message. */ message?: string; - /** Optional progress percentage to display (value range: 0 to 100). If omitted no percentage will be shown. */ + /** Optional progress percentage to display (value range: 0 to 100). If omitted no percentage is shown. */ percentage?: number; }; } @@ -355,28 +354,28 @@ declare module DebugProtocol { /** Event message for 'progressUpdate' event type. The event signals that the progress reporting needs to be updated with a new message and/or percentage. The client does not have to update the UI immediately, but the clients needs to keep track of the message and/or percentage values. - This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. + This event should only be sent if the corresponding capability `supportsProgressReporting` is true. */ interface ProgressUpdateEvent extends Event { // event: 'progressUpdate'; body: { - /** The ID that was introduced in the initial 'progressStart' event. */ + /** The ID that was introduced in the initial `progressStart` event. */ progressId: string; /** Optional, more detailed progress message. If omitted, the previous message (if any) is used. */ message?: string; - /** Optional progress percentage to display (value range: 0 to 100). If omitted no percentage will be shown. */ + /** Optional progress percentage to display (value range: 0 to 100). If omitted no percentage is shown. */ percentage?: number; }; } /** Event message for 'progressEnd' event type. The event signals the end of the progress reporting with an optional final message. - This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. + This event should only be sent if the corresponding capability `supportsProgressReporting` is true. */ interface ProgressEndEvent extends Event { // event: 'progressEnd'; body: { - /** The ID that was introduced in the initial 'ProgressStartEvent'. */ + /** The ID that was introduced in the initial `ProgressStartEvent`. */ progressId: string; /** Optional, more detailed progress message. If omitted, the previous message (if any) is used. */ message?: string; @@ -386,22 +385,22 @@ declare module DebugProtocol { /** Event message for 'invalidated' event type. This event signals that some state in the debug adapter has changed and requires that the client needs to re-render the data snapshot previously requested. Debug adapters do not have to emit this event for runtime changes like stopped or thread events because in that case the client refetches the new state anyway. But the event can be used for example to refresh the UI after rendering formatting has changed in the debug adapter. - This event should only be sent if the debug adapter has received a value true for the 'supportsInvalidatedEvent' capability of the 'initialize' request. + This event should only be sent if the corresponding capability `supportsInvalidatedEvent` is true. */ interface InvalidatedEvent extends Event { // event: 'invalidated'; body: { - /** Optional set of logical areas that got invalidated. This property has a hint characteristic: a client can only be expected to make a 'best effort' in honouring the areas but there are no guarantees. If this property is missing, empty, or if values are not understood, the client should assume a single value 'all'. */ + /** Optional set of logical areas that got invalidated. This property has a hint characteristic: a client can only be expected to make a 'best effort' in honouring the areas but there are no guarantees. If this property is missing, empty, or if values are not understood, the client should assume a single value `all`. */ areas?: InvalidatedAreas[]; /** If specified, the client only needs to refetch data related to this thread. */ threadId?: number; - /** If specified, the client only needs to refetch data related to this stack frame (and the 'threadId' is ignored). */ + /** If specified, the client only needs to refetch data related to this stack frame (and the `threadId` is ignored). */ stackFrameId?: number; }; } /** Event message for 'memory' event type. - This event indicates that some memory range has been updated. It should only be sent if the debug adapter has received a value true for the `supportsMemoryEvent` capability of the `initialize` request. + This event indicates that some memory range has been updated. It should only be sent if the corresponding capability `supportsMemoryEvent` is true. Clients typically react to the event by re-issuing a `readMemory` request if they show the memory identified by the `memoryReference` and if the updated memory range overlaps the displayed range. Clients should not make assumptions how individual memory references relate to each other, so they should not assume that they are part of a single continuous address range and might overlap. Debug adapters can use this event to indicate that the contents of a memory range has changed due to some other request like `setVariable` or `setExpression`. Debug adapters are not expected to emit this event for each and every memory change of a running program, because that information is typically not available from debuggers and it would flood clients with too many events. */ @@ -420,14 +419,16 @@ declare module DebugProtocol { /** RunInTerminal request; value of command field is 'runInTerminal'. This optional request is sent from the debug adapter to the client to run a command in a terminal. This is typically used to launch the debuggee in a terminal provided by the client. - This request should only be called if the client has passed the value true for the 'supportsRunInTerminalRequest' capability of the 'initialize' request. + This request should only be called if the corresponding capability `supportsRunInTerminalRequest` is true. + Client implementations of `runInTerminal` are free to run the command however they choose including issuing the command to a command line interpreter (aka 'shell'). Argument strings passed to the `runInTerminal` request must arrive verbatim in the command to be run. As a consequence, clients which use a shell are responsible for escaping any special shell characters in the argument strings to prevent them from being interpreted (and modified) by the shell. + Some users may wish to take advantage of shell processing in the argument strings. For clients which implement `runInTerminal` using an intermediary shell, the `argsCanBeInterpretedByShell` property can be set to true. In this case the client is requested not to escape any special shell characters in the argument strings. */ interface RunInTerminalRequest extends Request { // command: 'runInTerminal'; arguments: RunInTerminalRequestArguments; } - /** Arguments for 'runInTerminal' request. */ + /** Arguments for `runInTerminal` request. */ interface RunInTerminalRequestArguments { /** What kind of terminal to launch. */ kind?: 'integrated' | 'external'; @@ -439,9 +440,11 @@ declare module DebugProtocol { args: string[]; /** Environment key-value pairs that are added to or removed from the default environment. */ env?: { [key: string]: string | null; }; + /** This property should only be set if the corresponding capability `supportsArgsCanBeInterpretedByShell` is true. If the client uses an intermediary shell to launch the application, then the client must not attempt to escape characters with special meanings for the shell. The user is fully responsible for escaping as needed and that arguments using special characters may not be portable across shells. */ + argsCanBeInterpretedByShell?: boolean; } - /** Response to 'runInTerminal' request. */ + /** Response to `runInTerminal` request. */ interface RunInTerminalResponse extends Response { body: { /** The process ID. The value should be less than or equal to 2147483647 (2^31-1). */ @@ -452,17 +455,17 @@ declare module DebugProtocol { } /** Initialize request; value of command field is 'initialize'. - The 'initialize' request is sent as the first request from the client to the debug adapter in order to configure it with client capabilities and to retrieve capabilities from the debug adapter. - Until the debug adapter has responded to with an 'initialize' response, the client must not send any additional requests or events to the debug adapter. - In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an 'initialize' response. - The 'initialize' request may only be sent once. + The `initialize` request is sent as the first request from the client to the debug adapter in order to configure it with client capabilities and to retrieve capabilities from the debug adapter. + Until the debug adapter has responded to with an `initialize` response, the client must not send any additional requests or events to the debug adapter. + In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an `initialize` response. + The `initialize` request may only be sent once. */ interface InitializeRequest extends Request { // command: 'initialize'; arguments: InitializeRequestArguments; } - /** Arguments for 'initialize' request. */ + /** Arguments for `initialize` request. */ interface InitializeRequestArguments { /** The ID of the client using this adapter. */ clientID?: string; @@ -476,7 +479,7 @@ declare module DebugProtocol { linesStartAt1?: boolean; /** If true all column numbers are 1-based (default). */ columnsStartAt1?: boolean; - /** Determines in what format paths are specified. The default is 'path', which is the native format. + /** Determines in what format paths are specified. The default is `path`, which is the native format. Values: 'path', 'uri', etc. */ pathFormat?: 'path' | 'uri' | string; @@ -494,9 +497,11 @@ declare module DebugProtocol { supportsInvalidatedEvent?: boolean; /** Client supports the memory event. */ supportsMemoryEvent?: boolean; + /** Client supports the 'argsCanBeInterpretedByShell' attribute on the 'runInTerminal' request. */ + supportsArgsCanBeInterpretedByShell?: boolean; } - /** Response to 'initialize' request. */ + /** Response to `initialize` request. */ interface InitializeResponse extends Response { /** The capabilities of this debug adapter. */ body?: Capabilities; @@ -504,24 +509,24 @@ declare module DebugProtocol { /** ConfigurationDone request; value of command field is 'configurationDone'. This optional request indicates that the client has finished initialization of the debug adapter. - So it is the last request in the sequence of configuration requests (which was started by the 'initialized' event). - Clients should only call this request if the capability 'supportsConfigurationDoneRequest' is true. + So it is the last request in the sequence of configuration requests (which was started by the `initialized` event). + Clients should only call this request if the corresponding capability `supportsConfigurationDoneRequest` is true. */ interface ConfigurationDoneRequest extends Request { // command: 'configurationDone'; arguments?: ConfigurationDoneArguments; } - /** Arguments for 'configurationDone' request. */ + /** Arguments for `configurationDone` request. */ interface ConfigurationDoneArguments { } - /** Response to 'configurationDone' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `configurationDone` request. This is just an acknowledgement, so no body field is required. */ interface ConfigurationDoneResponse extends Response { } /** Launch request; value of command field is 'launch'. - This launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if 'noDebug' is true). + This launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if `noDebug` is true). Since launching is debugger/runtime specific, the arguments for this request are not part of this specification. */ interface LaunchRequest extends Request { @@ -529,18 +534,18 @@ declare module DebugProtocol { arguments: LaunchRequestArguments; } - /** Arguments for 'launch' request. Additional attributes are implementation specific. */ + /** Arguments for `launch` request. Additional attributes are implementation specific. */ interface LaunchRequestArguments { /** If noDebug is true, the launch request should launch the program without enabling debugging. */ noDebug?: boolean; /** Optional data from the previous, restarted session. - The data is sent as the 'restart' attribute of the 'terminated' event. + The data is sent as the `restart` attribute of the `terminated` event. The client should leave the data intact. */ __restart?: any; } - /** Response to 'launch' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `launch` request. This is just an acknowledgement, so no body field is required. */ interface LaunchResponse extends Response { } @@ -553,101 +558,101 @@ declare module DebugProtocol { arguments: AttachRequestArguments; } - /** Arguments for 'attach' request. Additional attributes are implementation specific. */ + /** Arguments for `attach` request. Additional attributes are implementation specific. */ interface AttachRequestArguments { /** Optional data from the previous, restarted session. - The data is sent as the 'restart' attribute of the 'terminated' event. + The data is sent as the `restart` attribute of the `terminated` event. The client should leave the data intact. */ __restart?: any; } - /** Response to 'attach' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `attach` request. This is just an acknowledgement, so no body field is required. */ interface AttachResponse extends Response { } /** Restart request; value of command field is 'restart'. - Restarts a debug session. Clients should only call this request if the capability 'supportsRestartRequest' is true. - If the capability is missing or has the value false, a typical client will emulate 'restart' by terminating the debug adapter first and then launching it anew. + Restarts a debug session. Clients should only call this request if the corresponding capability `supportsRestartRequest` is true. + If the capability is missing or has the value false, a typical client emulates `restart` by terminating the debug adapter first and then launching it anew. */ interface RestartRequest extends Request { // command: 'restart'; arguments?: RestartArguments; } - /** Arguments for 'restart' request. */ + /** Arguments for `restart` request. */ interface RestartArguments { - /** The latest version of the 'launch' or 'attach' configuration. */ + /** The latest version of the `launch` or `attach` configuration. */ arguments?: LaunchRequestArguments | AttachRequestArguments; } - /** Response to 'restart' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `restart` request. This is just an acknowledgement, so no body field is required. */ interface RestartResponse extends Response { } /** Disconnect request; value of command field is 'disconnect'. - The 'disconnect' request asks the debug adapter to disconnect from the debuggee (thus ending the debug session) and then to shut down itself (the debug adapter). - In addition, the debug adapter must terminate the debuggee if it was started with the 'launch' request. If an 'attach' request was used to connect to the debuggee, then the debug adapter must not terminate the debuggee. - This implicit behavior of when to terminate the debuggee can be overridden with the optional argument 'terminateDebuggee' (which is only supported by a debug adapter if the corresponding capability 'supportTerminateDebuggee' is true). + The `disconnect` request asks the debug adapter to disconnect from the debuggee (thus ending the debug session) and then to shut down itself (the debug adapter). + In addition, the debug adapter must terminate the debuggee if it was started with the `launch` request. If an `attach` request was used to connect to the debuggee, then the debug adapter must not terminate the debuggee. + This implicit behavior of when to terminate the debuggee can be overridden with the optional argument `terminateDebuggee` (which is only supported by a debug adapter if the corresponding capability `supportTerminateDebuggee` is true). */ interface DisconnectRequest extends Request { // command: 'disconnect'; arguments?: DisconnectArguments; } - /** Arguments for 'disconnect' request. */ + /** Arguments for `disconnect` request. */ interface DisconnectArguments { - /** A value of true indicates that this 'disconnect' request is part of a restart sequence. */ + /** A value of true indicates that this `disconnect` request is part of a restart sequence. */ restart?: boolean; /** Indicates whether the debuggee should be terminated when the debugger is disconnected. If unspecified, the debug adapter is free to do whatever it thinks is best. - The attribute is only honored by a debug adapter if the capability 'supportTerminateDebuggee' is true. + The attribute is only honored by a debug adapter if the corresponding capability `supportTerminateDebuggee` is true. */ terminateDebuggee?: boolean; /** Indicates whether the debuggee should stay suspended when the debugger is disconnected. If unspecified, the debuggee should resume execution. - The attribute is only honored by a debug adapter if the capability 'supportSuspendDebuggee' is true. + The attribute is only honored by a debug adapter if the corresponding capability `supportSuspendDebuggee` is true. */ suspendDebuggee?: boolean; } - /** Response to 'disconnect' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `disconnect` request. This is just an acknowledgement, so no body field is required. */ interface DisconnectResponse extends Response { } /** Terminate request; value of command field is 'terminate'. - The 'terminate' request is sent from the client to the debug adapter in order to shut down the debuggee gracefully. Clients should only call this request if the capability 'supportsTerminateRequest' is true. - Typically a debug adapter implements 'terminate' by sending a software signal which the debuggee intercepts in order to clean things up properly before terminating itself. - Please note that this request does not directly affect the state of the debug session: if the debuggee decides to veto the graceful shutdown for any reason by not terminating itself, then the debug session will just continue. - Clients can surface the 'terminate' request as an explicit command or they can integrate it into a two stage Stop command that first sends 'terminate' to request a graceful shutdown, and if that fails uses 'disconnect' for a forceful shutdown. + The `terminate` request is sent from the client to the debug adapter in order to shut down the debuggee gracefully. Clients should only call this request if the capability `supportsTerminateRequest` is true. + Typically a debug adapter implements `terminate` by sending a software signal which the debuggee intercepts in order to clean things up properly before terminating itself. + Please note that this request does not directly affect the state of the debug session: if the debuggee decides to veto the graceful shutdown for any reason by not terminating itself, then the debug session just continues. + Clients can surface the `terminate` request as an explicit command or they can integrate it into a two stage Stop command that first sends `terminate` to request a graceful shutdown, and if that fails uses `disconnect` for a forceful shutdown. */ interface TerminateRequest extends Request { // command: 'terminate'; arguments?: TerminateArguments; } - /** Arguments for 'terminate' request. */ + /** Arguments for `terminate` request. */ interface TerminateArguments { - /** A value of true indicates that this 'terminate' request is part of a restart sequence. */ + /** A value of true indicates that this `terminate` request is part of a restart sequence. */ restart?: boolean; } - /** Response to 'terminate' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `terminate` request. This is just an acknowledgement, so no body field is required. */ interface TerminateResponse extends Response { } /** BreakpointLocations request; value of command field is 'breakpointLocations'. - The 'breakpointLocations' request returns all possible locations for source breakpoints in a given range. - Clients should only call this request if the capability 'supportsBreakpointLocationsRequest' is true. + The `breakpointLocations` request returns all possible locations for source breakpoints in a given range. + Clients should only call this request if the corresponding capability `supportsBreakpointLocationsRequest` is true. */ interface BreakpointLocationsRequest extends Request { // command: 'breakpointLocations'; arguments?: BreakpointLocationsArguments; } - /** Arguments for 'breakpointLocations' request. */ + /** Arguments for `breakpointLocations` request. */ interface BreakpointLocationsArguments { - /** The source location of the breakpoints; either 'source.path' or 'source.reference' must be specified. */ + /** The source location of the breakpoints; either `source.path` or `source.reference` must be specified. */ source: Source; /** Start line of range to search possible breakpoint locations in. If only the line is specified, the request returns all possible locations in that line. */ line: number; @@ -659,7 +664,7 @@ declare module DebugProtocol { endColumn?: number; } - /** Response to 'breakpointLocations' request. + /** Response to `breakpointLocations` request. Contains possible locations for source breakpoints. */ interface BreakpointLocationsResponse extends Response { @@ -672,16 +677,16 @@ declare module DebugProtocol { /** SetBreakpoints request; value of command field is 'setBreakpoints'. Sets multiple breakpoints for a single source and clears all previous breakpoints in that source. To clear all breakpoint for a source, specify an empty array. - When a breakpoint is hit, a 'stopped' event (with reason 'breakpoint') is generated. + When a breakpoint is hit, a `stopped` event (with reason `breakpoint`) is generated. */ interface SetBreakpointsRequest extends Request { // command: 'setBreakpoints'; arguments: SetBreakpointsArguments; } - /** Arguments for 'setBreakpoints' request. */ + /** Arguments for `setBreakpoints` request. */ interface SetBreakpointsArguments { - /** The source location of the breakpoints; either 'source.path' or 'source.sourceReference' must be specified. */ + /** The source location of the breakpoints; either `source.path` or `source.sourceReference` must be specified. */ source: Source; /** The code locations of the breakpoints. */ breakpoints?: SourceBreakpoint[]; @@ -691,16 +696,16 @@ declare module DebugProtocol { sourceModified?: boolean; } - /** Response to 'setBreakpoints' request. + /** Response to `setBreakpoints` request. Returned is information about each breakpoint created by this request. This includes the actual code location and whether the breakpoint could be verified. - The breakpoints returned are in the same order as the elements of the 'breakpoints' - (or the deprecated 'lines') array in the arguments. + The breakpoints returned are in the same order as the elements of the `breakpoints` + (or the deprecated `lines`) array in the arguments. */ interface SetBreakpointsResponse extends Response { body: { /** Information about the breakpoints. - The array elements are in the same order as the elements of the 'breakpoints' (or the deprecated 'lines') array in the arguments. + The array elements are in the same order as the elements of the `breakpoints` (or the deprecated `lines`) array in the arguments. */ breakpoints: Breakpoint[]; }; @@ -709,61 +714,61 @@ declare module DebugProtocol { /** SetFunctionBreakpoints request; value of command field is 'setFunctionBreakpoints'. Replaces all existing function breakpoints with new function breakpoints. To clear all function breakpoints, specify an empty array. - When a function breakpoint is hit, a 'stopped' event (with reason 'function breakpoint') is generated. - Clients should only call this request if the capability 'supportsFunctionBreakpoints' is true. + When a function breakpoint is hit, a `stopped` event (with reason `function breakpoint`) is generated. + Clients should only call this request if the corresponding capability `supportsFunctionBreakpoints` is true. */ interface SetFunctionBreakpointsRequest extends Request { // command: 'setFunctionBreakpoints'; arguments: SetFunctionBreakpointsArguments; } - /** Arguments for 'setFunctionBreakpoints' request. */ + /** Arguments for `setFunctionBreakpoints` request. */ interface SetFunctionBreakpointsArguments { /** The function names of the breakpoints. */ breakpoints: FunctionBreakpoint[]; } - /** Response to 'setFunctionBreakpoints' request. + /** Response to `setFunctionBreakpoints` request. Returned is information about each breakpoint created by this request. */ interface SetFunctionBreakpointsResponse extends Response { body: { - /** Information about the breakpoints. The array elements correspond to the elements of the 'breakpoints' array. */ + /** Information about the breakpoints. The array elements correspond to the elements of the `breakpoints` array. */ breakpoints: Breakpoint[]; }; } /** SetExceptionBreakpoints request; value of command field is 'setExceptionBreakpoints'. The request configures the debugger's response to thrown exceptions. - If an exception is configured to break, a 'stopped' event is fired (with reason 'exception'). - Clients should only call this request if the capability 'exceptionBreakpointFilters' returns one or more filters. + If an exception is configured to break, a `stopped` event is fired (with reason `exception`). + Clients should only call this request if the corresponding capability `exceptionBreakpointFilters` returns one or more filters. */ interface SetExceptionBreakpointsRequest extends Request { // command: 'setExceptionBreakpoints'; arguments: SetExceptionBreakpointsArguments; } - /** Arguments for 'setExceptionBreakpoints' request. */ + /** Arguments for `setExceptionBreakpoints` request. */ interface SetExceptionBreakpointsArguments { - /** Set of exception filters specified by their ID. The set of all possible exception filters is defined by the 'exceptionBreakpointFilters' capability. The 'filter' and 'filterOptions' sets are additive. */ + /** Set of exception filters specified by their ID. The set of all possible exception filters is defined by the `exceptionBreakpointFilters` capability. The `filter` and `filterOptions` sets are additive. */ filters: string[]; - /** Set of exception filters and their options. The set of all possible exception filters is defined by the 'exceptionBreakpointFilters' capability. This attribute is only honored by a debug adapter if the capability 'supportsExceptionFilterOptions' is true. The 'filter' and 'filterOptions' sets are additive. */ + /** Set of exception filters and their options. The set of all possible exception filters is defined by the `exceptionBreakpointFilters` capability. This attribute is only honored by a debug adapter if the corresponding capability `supportsExceptionFilterOptions` is true. The `filter` and `filterOptions` sets are additive. */ filterOptions?: ExceptionFilterOptions[]; /** Configuration options for selected exceptions. - The attribute is only honored by a debug adapter if the capability 'supportsExceptionOptions' is true. + The attribute is only honored by a debug adapter if the corresponding capability `supportsExceptionOptions` is true. */ exceptionOptions?: ExceptionOptions[]; } - /** Response to 'setExceptionBreakpoints' request. - The response contains an array of Breakpoint objects with information about each exception breakpoint or filter. The Breakpoint objects are in the same order as the elements of the 'filters', 'filterOptions', 'exceptionOptions' arrays given as arguments. If both 'filters' and 'filterOptions' are given, the returned array must start with 'filters' information first, followed by 'filterOptions' information. - The mandatory 'verified' property of a Breakpoint object signals whether the exception breakpoint or filter could be successfully created and whether the optional condition or hit count expressions are valid. In case of an error the 'message' property explains the problem. An optional 'id' property can be used to introduce a unique ID for the exception breakpoint or filter so that it can be updated subsequently by sending breakpoint events. - For backward compatibility both the 'breakpoints' array and the enclosing 'body' are optional. If these elements are missing a client will not be able to show problems for individual exception breakpoints or filters. + /** Response to `setExceptionBreakpoints` request. + The response contains an array of `Breakpoint` objects with information about each exception breakpoint or filter. The `Breakpoint` objects are in the same order as the elements of the `filters`, `filterOptions`, `exceptionOptions` arrays given as arguments. If both `filters` and `filterOptions` are given, the returned array must start with `filters` information first, followed by `filterOptions` information. + The mandatory `verified` property of a `Breakpoint` object signals whether the exception breakpoint or filter could be successfully created and whether the optional condition or hit count expressions are valid. In case of an error the `message` property explains the problem. An optional `id` property can be used to introduce a unique ID for the exception breakpoint or filter so that it can be updated subsequently by sending breakpoint events. + For backward compatibility both the `breakpoints` array and the enclosing `body` are optional. If these elements are missing a client is not able to show problems for individual exception breakpoints or filters. */ interface SetExceptionBreakpointsResponse extends Response { body?: { /** Information about the exception breakpoints or filters. - The breakpoints returned are in the same order as the elements of the 'filters', 'filterOptions', 'exceptionOptions' arrays in the arguments. If both 'filters' and 'filterOptions' are given, the returned array must start with 'filters' information first, followed by 'filterOptions' information. + The breakpoints returned are in the same order as the elements of the `filters`, `filterOptions`, `exceptionOptions` arrays in the arguments. If both `filters` and `filterOptions` are given, the returned array must start with `filters` information first, followed by `filterOptions` information. */ breakpoints?: Breakpoint[]; }; @@ -771,24 +776,24 @@ declare module DebugProtocol { /** DataBreakpointInfo request; value of command field is 'dataBreakpointInfo'. Obtains information on a possible data breakpoint that could be set on an expression or variable. - Clients should only call this request if the capability 'supportsDataBreakpoints' is true. + Clients should only call this request if the corresponding capability `supportsDataBreakpoints` is true. */ interface DataBreakpointInfoRequest extends Request { // command: 'dataBreakpointInfo'; arguments: DataBreakpointInfoArguments; } - /** Arguments for 'dataBreakpointInfo' request. */ + /** Arguments for `dataBreakpointInfo` request. */ interface DataBreakpointInfoArguments { /** Reference to the Variable container if the data breakpoint is requested for a child of the container. */ variablesReference?: number; - /** The name of the Variable's child to obtain data breakpoint information for. + /** The name of the variable's child to obtain data breakpoint information for. If variablesReference isn't provided, this can be an expression. */ name: string; } - /** Response to 'dataBreakpointInfo' request. */ + /** Response to `dataBreakpointInfo` request. */ interface DataBreakpointInfoResponse extends Response { body: { /** An identifier for the data on which a data breakpoint can be registered with the setDataBreakpoints request or null if no data breakpoint is available. */ @@ -805,26 +810,26 @@ declare module DebugProtocol { /** SetDataBreakpoints request; value of command field is 'setDataBreakpoints'. Replaces all existing data breakpoints with new data breakpoints. To clear all data breakpoints, specify an empty array. - When a data breakpoint is hit, a 'stopped' event (with reason 'data breakpoint') is generated. - Clients should only call this request if the capability 'supportsDataBreakpoints' is true. + When a data breakpoint is hit, a `stopped` event (with reason `data breakpoint`) is generated. + Clients should only call this request if the corresponding capability `supportsDataBreakpoints` is true. */ interface SetDataBreakpointsRequest extends Request { // command: 'setDataBreakpoints'; arguments: SetDataBreakpointsArguments; } - /** Arguments for 'setDataBreakpoints' request. */ + /** Arguments for `setDataBreakpoints` request. */ interface SetDataBreakpointsArguments { /** The contents of this array replaces all existing data breakpoints. An empty array clears all data breakpoints. */ breakpoints: DataBreakpoint[]; } - /** Response to 'setDataBreakpoints' request. + /** Response to `setDataBreakpoints` request. Returned is information about each breakpoint created by this request. */ interface SetDataBreakpointsResponse extends Response { body: { - /** Information about the data breakpoints. The array elements correspond to the elements of the input argument 'breakpoints' array. */ + /** Information about the data breakpoints. The array elements correspond to the elements of the input argument `breakpoints` array. */ breakpoints: Breakpoint[]; }; } @@ -832,45 +837,45 @@ declare module DebugProtocol { /** SetInstructionBreakpoints request; value of command field is 'setInstructionBreakpoints'. Replaces all existing instruction breakpoints. Typically, instruction breakpoints would be set from a disassembly window. To clear all instruction breakpoints, specify an empty array. - When an instruction breakpoint is hit, a 'stopped' event (with reason 'instruction breakpoint') is generated. - Clients should only call this request if the capability 'supportsInstructionBreakpoints' is true. + When an instruction breakpoint is hit, a `stopped` event (with reason `instruction breakpoint`) is generated. + Clients should only call this request if the corresponding capability `supportsInstructionBreakpoints` is true. */ interface SetInstructionBreakpointsRequest extends Request { // command: 'setInstructionBreakpoints'; arguments: SetInstructionBreakpointsArguments; } - /** Arguments for 'setInstructionBreakpoints' request */ + /** Arguments for `setInstructionBreakpoints` request */ interface SetInstructionBreakpointsArguments { /** The instruction references of the breakpoints */ breakpoints: InstructionBreakpoint[]; } - /** Response to 'setInstructionBreakpoints' request */ + /** Response to `setInstructionBreakpoints` request */ interface SetInstructionBreakpointsResponse extends Response { body: { - /** Information about the breakpoints. The array elements correspond to the elements of the 'breakpoints' array. */ + /** Information about the breakpoints. The array elements correspond to the elements of the `breakpoints` array. */ breakpoints: Breakpoint[]; }; } /** Continue request; value of command field is 'continue'. - The request resumes execution of all threads. If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true resumes only the specified thread. If not all threads were resumed, the 'allThreadsContinued' attribute of the response must be set to false. + The request resumes execution of all threads. If the debug adapter supports single thread execution (see capability `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument to true resumes only the specified thread. If not all threads were resumed, the `allThreadsContinued` attribute of the response must be set to false. */ interface ContinueRequest extends Request { // command: 'continue'; arguments: ContinueArguments; } - /** Arguments for 'continue' request. */ + /** Arguments for `continue` request. */ interface ContinueArguments { - /** Specifies the active thread. If the debug adapter supports single thread execution (see 'supportsSingleThreadExecutionRequests') and the optional argument 'singleThread' is true, only the thread with this ID is resumed. */ + /** Specifies the active thread. If the debug adapter supports single thread execution (see `supportsSingleThreadExecutionRequests`) and the optional argument `singleThread` is true, only the thread with this ID is resumed. */ threadId: number; - /** If this optional flag is true, execution is resumed only for the thread with given 'threadId'. */ + /** If this optional flag is true, execution is resumed only for the thread with given `threadId`. */ singleThread?: boolean; } - /** Response to 'continue' request. */ + /** Response to `continue` request. */ interface ContinueResponse extends Response { body: { /** The value true (or a missing property) signals to the client that all threads have been resumed. The value false must be returned if not all threads were resumed. */ @@ -880,43 +885,43 @@ declare module DebugProtocol { /** Next request; value of command field is 'next'. The request executes one step (in the given granularity) for the specified thread and allows all other threads to run freely by resuming them. - If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true prevents other suspended threads from resuming. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. + If the debug adapter supports single thread execution (see capability `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument to true prevents other suspended threads from resuming. + The debug adapter first sends the response and then a `stopped` event (with reason `step`) after the step has completed. */ interface NextRequest extends Request { // command: 'next'; arguments: NextArguments; } - /** Arguments for 'next' request. */ + /** Arguments for `next` request. */ interface NextArguments { /** Specifies the thread for which to resume execution for one step (of the given granularity). */ threadId: number; /** If this optional flag is true, all other suspended threads are not resumed. */ singleThread?: boolean; - /** Optional granularity to step. If no granularity is specified, a granularity of 'statement' is assumed. */ + /** Optional granularity to step. If no granularity is specified, a granularity of `statement` is assumed. */ granularity?: SteppingGranularity; } - /** Response to 'next' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `next` request. This is just an acknowledgement, so no body field is required. */ interface NextResponse extends Response { } /** StepIn request; value of command field is 'stepIn'. The request resumes the given thread to step into a function/method and allows all other threads to run freely by resuming them. - If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true prevents other suspended threads from resuming. - If the request cannot step into a target, 'stepIn' behaves like the 'next' request. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. + If the debug adapter supports single thread execution (see capability `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument to true prevents other suspended threads from resuming. + If the request cannot step into a target, `stepIn` behaves like the `next` request. + The debug adapter first sends the response and then a `stopped` event (with reason `step`) after the step has completed. If there are multiple function/method calls (or other targets) on the source line, - the optional argument 'targetId' can be used to control into which target the 'stepIn' should occur. - The list of possible targets for a given source line can be retrieved via the 'stepInTargets' request. + the optional argument `targetId` can be used to control into which target the `stepIn` should occur. + The list of possible targets for a given source line can be retrieved via the `stepInTargets` request. */ interface StepInRequest extends Request { // command: 'stepIn'; arguments: StepInArguments; } - /** Arguments for 'stepIn' request. */ + /** Arguments for `stepIn` request. */ interface StepInArguments { /** Specifies the thread for which to resume execution for one step-into (of the given granularity). */ threadId: number; @@ -924,101 +929,101 @@ declare module DebugProtocol { singleThread?: boolean; /** Optional id of the target to step into. */ targetId?: number; - /** Optional granularity to step. If no granularity is specified, a granularity of 'statement' is assumed. */ + /** Optional granularity to step. If no granularity is specified, a granularity of `statement` is assumed. */ granularity?: SteppingGranularity; } - /** Response to 'stepIn' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `stepIn` request. This is just an acknowledgement, so no body field is required. */ interface StepInResponse extends Response { } /** StepOut request; value of command field is 'stepOut'. The request resumes the given thread to step out (return) from a function/method and allows all other threads to run freely by resuming them. - If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true prevents other suspended threads from resuming. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. + If the debug adapter supports single thread execution (see capability `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument to true prevents other suspended threads from resuming. + The debug adapter first sends the response and then a `stopped` event (with reason `step`) after the step has completed. */ interface StepOutRequest extends Request { // command: 'stepOut'; arguments: StepOutArguments; } - /** Arguments for 'stepOut' request. */ + /** Arguments for `stepOut` request. */ interface StepOutArguments { /** Specifies the thread for which to resume execution for one step-out (of the given granularity). */ threadId: number; /** If this optional flag is true, all other suspended threads are not resumed. */ singleThread?: boolean; - /** Optional granularity to step. If no granularity is specified, a granularity of 'statement' is assumed. */ + /** Optional granularity to step. If no granularity is specified, a granularity of `statement` is assumed. */ granularity?: SteppingGranularity; } - /** Response to 'stepOut' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `stepOut` request. This is just an acknowledgement, so no body field is required. */ interface StepOutResponse extends Response { } /** StepBack request; value of command field is 'stepBack'. The request executes one backward step (in the given granularity) for the specified thread and allows all other threads to run backward freely by resuming them. - If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true prevents other suspended threads from resuming. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. - Clients should only call this request if the capability 'supportsStepBack' is true. + If the debug adapter supports single thread execution (see capability `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument to true prevents other suspended threads from resuming. + The debug adapter first sends the response and then a `stopped` event (with reason `step`) after the step has completed. + Clients should only call this request if the corresponding capability `supportsStepBack` is true. */ interface StepBackRequest extends Request { // command: 'stepBack'; arguments: StepBackArguments; } - /** Arguments for 'stepBack' request. */ + /** Arguments for `stepBack` request. */ interface StepBackArguments { /** Specifies the thread for which to resume execution for one step backwards (of the given granularity). */ threadId: number; /** If this optional flag is true, all other suspended threads are not resumed. */ singleThread?: boolean; - /** Optional granularity to step. If no granularity is specified, a granularity of 'statement' is assumed. */ + /** Optional granularity to step. If no granularity is specified, a granularity of `statement` is assumed. */ granularity?: SteppingGranularity; } - /** Response to 'stepBack' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `stepBack` request. This is just an acknowledgement, so no body field is required. */ interface StepBackResponse extends Response { } /** ReverseContinue request; value of command field is 'reverseContinue'. - The request resumes backward execution of all threads. If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests'), setting the 'singleThread' argument to true resumes only the specified thread. If not all threads were resumed, the 'allThreadsContinued' attribute of the response must be set to false. - Clients should only call this request if the capability 'supportsStepBack' is true. + The request resumes backward execution of all threads. If the debug adapter supports single thread execution (see capability `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument to true resumes only the specified thread. If not all threads were resumed, the `allThreadsContinued` attribute of the response must be set to false. + Clients should only call this request if the corresponding capability `supportsStepBack` is true. */ interface ReverseContinueRequest extends Request { // command: 'reverseContinue'; arguments: ReverseContinueArguments; } - /** Arguments for 'reverseContinue' request. */ + /** Arguments for `reverseContinue` request. */ interface ReverseContinueArguments { - /** Specifies the active thread. If the debug adapter supports single thread execution (see 'supportsSingleThreadExecutionRequests') and the optional argument 'singleThread' is true, only the thread with this ID is resumed. */ + /** Specifies the active thread. If the debug adapter supports single thread execution (see `supportsSingleThreadExecutionRequests`) and the optional argument `singleThread` is true, only the thread with this ID is resumed. */ threadId: number; - /** If this optional flag is true, backward execution is resumed only for the thread with given 'threadId'. */ + /** If this optional flag is true, backward execution is resumed only for the thread with given `threadId`. */ singleThread?: boolean; } - /** Response to 'reverseContinue' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `reverseContinue` request. This is just an acknowledgement, so no body field is required. */ interface ReverseContinueResponse extends Response { } /** RestartFrame request; value of command field is 'restartFrame'. The request restarts execution of the specified stackframe. - The debug adapter first sends the response and then a 'stopped' event (with reason 'restart') after the restart has completed. - Clients should only call this request if the capability 'supportsRestartFrame' is true. + The debug adapter first sends the response and then a `stopped` event (with reason `restart`) after the restart has completed. + Clients should only call this request if the corresponding capability `supportsRestartFrame` is true. */ interface RestartFrameRequest extends Request { // command: 'restartFrame'; arguments: RestartFrameArguments; } - /** Arguments for 'restartFrame' request. */ + /** Arguments for `restartFrame` request. */ interface RestartFrameArguments { /** Restart this stackframe. */ frameId: number; } - /** Response to 'restartFrame' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `restartFrame` request. This is just an acknowledgement, so no body field is required. */ interface RestartFrameResponse extends Response { } @@ -1026,15 +1031,15 @@ declare module DebugProtocol { The request sets the location where the debuggee will continue to run. This makes it possible to skip the execution of code or to execute code again. The code between the current location and the goto target is not executed but skipped. - The debug adapter first sends the response and then a 'stopped' event with reason 'goto'. - Clients should only call this request if the capability 'supportsGotoTargetsRequest' is true (because only then goto targets exist that can be passed as arguments). + The debug adapter first sends the response and then a `stopped` event with reason `goto`. + Clients should only call this request if the corresponding capability `supportsGotoTargetsRequest` is true (because only then goto targets exist that can be passed as arguments). */ interface GotoRequest extends Request { // command: 'goto'; arguments: GotoArguments; } - /** Arguments for 'goto' request. */ + /** Arguments for `goto` request. */ interface GotoArguments { /** Set the goto target for this thread. */ threadId: number; @@ -1042,39 +1047,39 @@ declare module DebugProtocol { targetId: number; } - /** Response to 'goto' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `goto` request. This is just an acknowledgement, so no body field is required. */ interface GotoResponse extends Response { } /** Pause request; value of command field is 'pause'. The request suspends the debuggee. - The debug adapter first sends the response and then a 'stopped' event (with reason 'pause') after the thread has been paused successfully. + The debug adapter first sends the response and then a `stopped` event (with reason `pause`) after the thread has been paused successfully. */ interface PauseRequest extends Request { // command: 'pause'; arguments: PauseArguments; } - /** Arguments for 'pause' request. */ + /** Arguments for `pause` request. */ interface PauseArguments { /** Pause execution for this thread. */ threadId: number; } - /** Response to 'pause' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `pause` request. This is just an acknowledgement, so no body field is required. */ interface PauseResponse extends Response { } /** StackTrace request; value of command field is 'stackTrace'. The request returns a stacktrace from the current execution state of a given thread. - A client can request all stack frames by omitting the startFrame and levels arguments. For performance-conscious clients and if the debug adapter's 'supportsDelayedStackTraceLoading' capability is true, stack frames can be retrieved in a piecemeal way with the startFrame and levels arguments. The response of the stackTrace request may contain a totalFrames property that hints at the total number of frames in the stack. If a client needs this total number upfront, it can issue a request for a single (first) frame and depending on the value of totalFrames decide how to proceed. In any case a client should be prepared to receive fewer frames than requested, which is an indication that the end of the stack has been reached. + A client can request all stack frames by omitting the startFrame and levels arguments. For performance-conscious clients and if the corresponding capability `supportsDelayedStackTraceLoading` is true, stack frames can be retrieved in a piecemeal way with the startFrame and levels arguments. The response of the stackTrace request may contain a totalFrames property that hints at the total number of frames in the stack. If a client needs this total number upfront, it can issue a request for a single (first) frame and depending on the value of totalFrames decide how to proceed. In any case a client should be prepared to receive fewer frames than requested, which is an indication that the end of the stack has been reached. */ interface StackTraceRequest extends Request { // command: 'stackTrace'; arguments: StackTraceArguments; } - /** Arguments for 'stackTrace' request. */ + /** Arguments for `stackTrace` request. */ interface StackTraceArguments { /** Retrieve the stacktrace for this thread. */ threadId: number; @@ -1083,12 +1088,12 @@ declare module DebugProtocol { /** The maximum number of frames to return. If levels is not specified or 0, all frames are returned. */ levels?: number; /** Specifies details on how to format the stack frames. - The attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true. + The attribute is only honored by a debug adapter if the corresponding capability `supportsValueFormattingOptions` is true. */ format?: StackFrameFormat; } - /** Response to 'stackTrace' request. */ + /** Response to `stackTrace` request. */ interface StackTraceResponse extends Response { body: { /** The frames of the stackframe. If the array has length zero, there are no stackframes available. @@ -1108,13 +1113,13 @@ declare module DebugProtocol { arguments: ScopesArguments; } - /** Arguments for 'scopes' request. */ + /** Arguments for `scopes` request. */ interface ScopesArguments { /** Retrieve the scopes for this stackframe. */ frameId: number; } - /** Response to 'scopes' request. */ + /** Response to `scopes` request. */ interface ScopesResponse extends Response { body: { /** The scopes of the stackframe. If the array has length zero, there are no scopes available. */ @@ -1131,7 +1136,7 @@ declare module DebugProtocol { arguments: VariablesArguments; } - /** Arguments for 'variables' request. */ + /** Arguments for `variables` request. */ interface VariablesArguments { /** The Variable reference. */ variablesReference: number; @@ -1142,12 +1147,12 @@ declare module DebugProtocol { /** The number of variables to return. If count is missing or 0, all variables are returned. */ count?: number; /** Specifies details on how to format the Variable values. - The attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true. + The attribute is only honored by a debug adapter if the corresponding capability `supportsValueFormattingOptions` is true. */ format?: ValueFormat; } - /** Response to 'variables' request. */ + /** Response to `variables` request. */ interface VariablesResponse extends Response { body: { /** All (or a range) of variables for the given variable reference. */ @@ -1156,15 +1161,15 @@ declare module DebugProtocol { } /** SetVariable request; value of command field is 'setVariable'. - Set the variable with the given name in the variable container to a new value. Clients should only call this request if the capability 'supportsSetVariable' is true. - If a debug adapter implements both setVariable and setExpression, a client will only use setExpression if the variable has an evaluateName property. + Set the variable with the given name in the variable container to a new value. Clients should only call this request if the corresponding capability `supportsSetVariable` is true. + If a debug adapter implements both `setVariable` and `setExpression`, a client will only use `setExpression` if the variable has an `evaluateName` property. */ interface SetVariableRequest extends Request { // command: 'setVariable'; arguments: SetVariableArguments; } - /** Arguments for 'setVariable' request. */ + /** Arguments for `setVariable` request. */ interface SetVariableArguments { /** The reference of the variable container. */ variablesReference: number; @@ -1176,7 +1181,7 @@ declare module DebugProtocol { format?: ValueFormat; } - /** Response to 'setVariable' request. */ + /** Response to `setVariable` request. */ interface SetVariableResponse extends Response { body: { /** The new value of the variable. */ @@ -1208,17 +1213,17 @@ declare module DebugProtocol { arguments: SourceArguments; } - /** Arguments for 'source' request. */ + /** Arguments for `source` request. */ interface SourceArguments { /** Specifies the source content to load. Either source.path or source.sourceReference must be specified. */ source?: Source; /** The reference to the source. This is the same as source.sourceReference. - This is provided for backward compatibility since old backends do not understand the 'source' attribute. + This is provided for backward compatibility since old clients do not understand the `source` attribute. */ sourceReference: number; } - /** Response to 'source' request. */ + /** Response to `source` request. */ interface SourceResponse extends Response { body: { /** Content of the source reference. */ @@ -1235,7 +1240,7 @@ declare module DebugProtocol { // command: 'threads'; } - /** Response to 'threads' request. */ + /** Response to `threads` request. */ interface ThreadsResponse extends Response { body: { /** All threads. */ @@ -1245,33 +1250,33 @@ declare module DebugProtocol { /** TerminateThreads request; value of command field is 'terminateThreads'. The request terminates the threads with the given ids. - Clients should only call this request if the capability 'supportsTerminateThreadsRequest' is true. + Clients should only call this request if the corresponding capability `supportsTerminateThreadsRequest` is true. */ interface TerminateThreadsRequest extends Request { // command: 'terminateThreads'; arguments: TerminateThreadsArguments; } - /** Arguments for 'terminateThreads' request. */ + /** Arguments for `terminateThreads` request. */ interface TerminateThreadsArguments { /** Ids of threads to be terminated. */ threadIds?: number[]; } - /** Response to 'terminateThreads' request. This is just an acknowledgement, so no body field is required. */ + /** Response to `terminateThreads` request. This is just an acknowledgement, so no body field is required. */ interface TerminateThreadsResponse extends Response { } /** Modules request; value of command field is 'modules'. Modules can be retrieved from the debug adapter with this request which can either return all modules or a range of modules to support paging. - Clients should only call this request if the capability 'supportsModulesRequest' is true. + Clients should only call this request if the corresponding capability `supportsModulesRequest` is true. */ interface ModulesRequest extends Request { // command: 'modules'; arguments: ModulesArguments; } - /** Arguments for 'modules' request. */ + /** Arguments for `modules` request. */ interface ModulesArguments { /** The index of the first module to return; if omitted modules start at 0. */ startModule?: number; @@ -1279,7 +1284,7 @@ declare module DebugProtocol { moduleCount?: number; } - /** Response to 'modules' request. */ + /** Response to `modules` request. */ interface ModulesResponse extends Response { body: { /** All modules or range of modules. */ @@ -1291,18 +1296,18 @@ declare module DebugProtocol { /** LoadedSources request; value of command field is 'loadedSources'. Retrieves the set of all sources currently loaded by the debugged process. - Clients should only call this request if the capability 'supportsLoadedSourcesRequest' is true. + Clients should only call this request if the corresponding capability `supportsLoadedSourcesRequest` is true. */ interface LoadedSourcesRequest extends Request { // command: 'loadedSources'; arguments?: LoadedSourcesArguments; } - /** Arguments for 'loadedSources' request. */ + /** Arguments for `loadedSources` request. */ interface LoadedSourcesArguments { } - /** Response to 'loadedSources' request. */ + /** Response to `loadedSources` request. */ interface LoadedSourcesResponse extends Response { body: { /** Set of loaded sources. */ @@ -1319,7 +1324,7 @@ declare module DebugProtocol { arguments: EvaluateArguments; } - /** Arguments for 'evaluate' request. */ + /** Arguments for `evaluate` request. */ interface EvaluateArguments { /** The expression to evaluate. */ expression: string; @@ -1331,25 +1336,25 @@ declare module DebugProtocol { 'watch': evaluate is called from a watch view context. 'repl': evaluate is called from a REPL context. 'hover': evaluate is called to generate the debug hover contents. - This value should only be used if the capability 'supportsEvaluateForHovers' is true. + This value should only be used if the corresponding capability `supportsEvaluateForHovers` is true. 'clipboard': evaluate is called to generate clipboard contents. - This value should only be used if the capability 'supportsClipboardContext' is true. + This value should only be used if the corresponding capability `supportsClipboardContext` is true. etc. */ context?: 'variables' | 'watch' | 'repl' | 'hover' | 'clipboard' | string; /** Specifies details on how to format the result. - The attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true. + The attribute is only honored by a debug adapter if the corresponding capability `supportsValueFormattingOptions` is true. */ format?: ValueFormat; } - /** Response to 'evaluate' request. */ + /** Response to `evaluate` request. */ interface EvaluateResponse extends Response { body: { /** The result of the evaluate request. */ result: string; /** The optional type of the evaluate result. - This attribute should only be returned by a debug adapter if the client has passed the value true for the 'supportsVariableType' capability of the 'initialize' request. + This attribute should only be returned by a debug adapter if the ccorresponding capability `supportsVariableType` is true. */ type?: string; /** Properties of an evaluate result that can be used to determine how to render the result in the UI. */ @@ -1370,24 +1375,24 @@ declare module DebugProtocol { indexedVariables?: number; /** Optional memory reference to a location appropriate for this result. For pointer type eval results, this is generally a reference to the memory address contained in the pointer. - This attribute should be returned by a debug adapter if the client has passed the value true for the 'supportsMemoryReferences' capability of the 'initialize' request. + This attribute should be returned by a debug adapter if corresponding capability `supportsMemoryReferences` is true. */ memoryReference?: string; }; } /** SetExpression request; value of command field is 'setExpression'. - Evaluates the given 'value' expression and assigns it to the 'expression' which must be a modifiable l-value. + Evaluates the given `value` expression and assigns it to the `expression` which must be a modifiable l-value. The expressions have access to any variables and arguments that are in scope of the specified frame. - Clients should only call this request if the capability 'supportsSetExpression' is true. - If a debug adapter implements both setExpression and setVariable, a client will only use setExpression if the variable has an evaluateName property. + Clients should only call this request if the corresponding capability `supportsSetExpression` is true. + If a debug adapter implements both setExpression and setVariable, a client uses `setExpression` if the variable has an `evaluateName` property. */ interface SetExpressionRequest extends Request { // command: 'setExpression'; arguments: SetExpressionArguments; } - /** Arguments for 'setExpression' request. */ + /** Arguments for `setExpression` request. */ interface SetExpressionArguments { /** The l-value expression to assign to. */ expression: string; @@ -1399,13 +1404,13 @@ declare module DebugProtocol { format?: ValueFormat; } - /** Response to 'setExpression' request. */ + /** Response to `setExpression` request. */ interface SetExpressionResponse extends Response { body: { /** The new value of the expression. */ value: string; /** The optional type of the value. - This attribute should only be returned by a debug adapter if the client has passed the value true for the 'supportsVariableType' capability of the 'initialize' request. + This attribute should only be returned by a debug adapter if the corresponding capability `supportsVariableType` is true. */ type?: string; /** Properties of a value that can be used to determine how to render the result in the UI. */ @@ -1429,22 +1434,21 @@ declare module DebugProtocol { /** StepInTargets request; value of command field is 'stepInTargets'. This request retrieves the possible stepIn targets for the specified stack frame. - These targets can be used in the 'stepIn' request. - The StepInTargets may only be called if the 'supportsStepInTargetsRequest' capability exists and is true. - Clients should only call this request if the capability 'supportsStepInTargetsRequest' is true. + These targets can be used in the `stepIn` request. + Clients should only call this request if the corresponding capability `supportsStepInTargetsRequest` is true. */ interface StepInTargetsRequest extends Request { // command: 'stepInTargets'; arguments: StepInTargetsArguments; } - /** Arguments for 'stepInTargets' request. */ + /** Arguments for `stepInTargets` request. */ interface StepInTargetsArguments { /** The stack frame for which to retrieve the possible stepIn targets. */ frameId: number; } - /** Response to 'stepInTargets' request. */ + /** Response to `stepInTargets` request. */ interface StepInTargetsResponse extends Response { body: { /** The possible stepIn targets of the specified source location. */ @@ -1454,15 +1458,15 @@ declare module DebugProtocol { /** GotoTargets request; value of command field is 'gotoTargets'. This request retrieves the possible goto targets for the specified source location. - These targets can be used in the 'goto' request. - Clients should only call this request if the capability 'supportsGotoTargetsRequest' is true. + These targets can be used in the `goto` request. + Clients should only call this request if the corresponding capability `supportsGotoTargetsRequest` is true. */ interface GotoTargetsRequest extends Request { // command: 'gotoTargets'; arguments: GotoTargetsArguments; } - /** Arguments for 'gotoTargets' request. */ + /** Arguments for `gotoTargets` request. */ interface GotoTargetsArguments { /** The source location for which the goto targets are determined. */ source: Source; @@ -1472,7 +1476,7 @@ declare module DebugProtocol { column?: number; } - /** Response to 'gotoTargets' request. */ + /** Response to `gotoTargets` request. */ interface GotoTargetsResponse extends Response { body: { /** The possible goto targets of the specified location. */ @@ -1482,14 +1486,14 @@ declare module DebugProtocol { /** Completions request; value of command field is 'completions'. Returns a list of possible completions for a given caret position and text. - Clients should only call this request if the capability 'supportsCompletionsRequest' is true. + Clients should only call this request if the corresponding capability `supportsCompletionsRequest` is true. */ interface CompletionsRequest extends Request { // command: 'completions'; arguments: CompletionsArguments; } - /** Arguments for 'completions' request. */ + /** Arguments for `completions` request. */ interface CompletionsArguments { /** Returns completions in the scope of this stack frame. If not specified, the completions are returned for the global scope. */ frameId?: number; @@ -1501,7 +1505,7 @@ declare module DebugProtocol { line?: number; } - /** Response to 'completions' request. */ + /** Response to `completions` request. */ interface CompletionsResponse extends Response { body: { /** The possible completions for . */ @@ -1511,25 +1515,25 @@ declare module DebugProtocol { /** ExceptionInfo request; value of command field is 'exceptionInfo'. Retrieves the details of the exception that caused this event to be raised. - Clients should only call this request if the capability 'supportsExceptionInfoRequest' is true. + Clients should only call this request if the corresponding capability `supportsExceptionInfoRequest` is true. */ interface ExceptionInfoRequest extends Request { // command: 'exceptionInfo'; arguments: ExceptionInfoArguments; } - /** Arguments for 'exceptionInfo' request. */ + /** Arguments for `exceptionInfo` request. */ interface ExceptionInfoArguments { /** Thread for which exception information should be retrieved. */ threadId: number; } - /** Response to 'exceptionInfo' request. */ + /** Response to `exceptionInfo` request. */ interface ExceptionInfoResponse extends Response { body: { /** ID of the exception that was thrown. */ exceptionId: string; - /** Descriptive text for the exception provided by the debug adapter. */ + /** Descriptive text for the exception. */ description?: string; /** Mode that caused the exception notification to be raised. */ breakMode: ExceptionBreakMode; @@ -1540,14 +1544,14 @@ declare module DebugProtocol { /** ReadMemory request; value of command field is 'readMemory'. Reads bytes from memory at the provided location. - Clients should only call this request if the capability 'supportsReadMemoryRequest' is true. + Clients should only call this request if the corresponding capability `supportsReadMemoryRequest` is true. */ interface ReadMemoryRequest extends Request { // command: 'readMemory'; arguments: ReadMemoryArguments; } - /** Arguments for 'readMemory' request. */ + /** Arguments for `readMemory` request. */ interface ReadMemoryArguments { /** Memory reference to the base location from which data should be read. */ memoryReference: string; @@ -1557,15 +1561,15 @@ declare module DebugProtocol { count: number; } - /** Response to 'readMemory' request. */ + /** Response to `readMemory` request. */ interface ReadMemoryResponse extends Response { body?: { /** The address of the first byte of data returned. - Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. + Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. */ address: string; /** The number of unreadable bytes encountered after the last successfully read byte. - This can be used to determine the number of bytes that must be skipped before a subsequent 'readMemory' request will succeed. + This can be used to determine the number of bytes that must be skipped before a subsequent `readMemory` request succeeds. */ unreadableBytes?: number; /** The bytes read from memory, encoded using base64. */ @@ -1575,20 +1579,20 @@ declare module DebugProtocol { /** WriteMemory request; value of command field is 'writeMemory'. Writes bytes to memory at the provided location. - Clients should only call this request if the capability 'supportsWriteMemoryRequest' is true. + Clients should only call this request if the corresponding capability `supportsWriteMemoryRequest` is true. */ interface WriteMemoryRequest extends Request { // command: 'writeMemory'; arguments: WriteMemoryArguments; } - /** Arguments for 'writeMemory' request. */ + /** Arguments for `writeMemory` request. */ interface WriteMemoryArguments { /** Memory reference to the base location to which data should be written. */ memoryReference: string; /** Optional offset (in bytes) to be applied to the reference location before writing data. Can be negative. */ offset?: number; - /** Optional property to control partial writes. If true, the debug adapter should attempt to write memory even if the entire memory region is not writable. In such a case the debug adapter should stop after hitting the first byte of memory that cannot be written and return the number of bytes written in the response via the 'offset' and 'bytesWritten' properties. + /** Optional property to control partial writes. If true, the debug adapter should attempt to write memory even if the entire memory region is not writable. In such a case the debug adapter should stop after hitting the first byte of memory that cannot be written and return the number of bytes written in the response via the `offset` and `bytesWritten` properties. If false or missing, a debug adapter should attempt to verify the region is writable before writing, and fail the response if it is not. */ allowPartial?: boolean; @@ -1596,26 +1600,26 @@ declare module DebugProtocol { data: string; } - /** Response to 'writeMemory' request. */ + /** Response to `writeMemory` request. */ interface WriteMemoryResponse extends Response { body?: { - /** Optional property that should be returned when 'allowPartial' is true to indicate the offset of the first byte of data successfully written. Can be negative. */ + /** Optional property that should be returned when `allowPartial` is true to indicate the offset of the first byte of data successfully written. Can be negative. */ offset?: number; - /** Optional property that should be returned when 'allowPartial' is true to indicate the number of bytes starting from address that were successfully written. */ + /** Optional property that should be returned when `allowPartial` is true to indicate the number of bytes starting from address that were successfully written. */ bytesWritten?: number; }; } /** Disassemble request; value of command field is 'disassemble'. Disassembles code stored at the provided location. - Clients should only call this request if the capability 'supportsDisassembleRequest' is true. + Clients should only call this request if the corresponding capability `supportsDisassembleRequest` is true. */ interface DisassembleRequest extends Request { // command: 'disassemble'; arguments: DisassembleArguments; } - /** Arguments for 'disassemble' request. */ + /** Arguments for `disassemble` request. */ interface DisassembleArguments { /** Memory reference to the base location containing the instructions to disassemble. */ memoryReference: string; @@ -1624,14 +1628,14 @@ declare module DebugProtocol { /** Optional offset (in instructions) to be applied after the byte offset (if any) before disassembling. Can be negative. */ instructionOffset?: number; /** Number of instructions to disassemble starting at the specified location and offset. - An adapter must return exactly this number of instructions - any unavailable instructions should be replaced with an implementation-defined 'invalid instruction' value. + An adapter must return exactly this number of instructions - any unavailable instructions should be replaced with an implementation-defined `invalid instruction` value. */ instructionCount: number; /** If true, the adapter should attempt to resolve memory addresses and other values to symbolic names. */ resolveSymbols?: boolean; } - /** Response to 'disassemble' request. */ + /** Response to `disassemble` request. */ interface DisassembleResponse extends Response { body?: { /** The list of disassembled instructions. */ @@ -1641,7 +1645,7 @@ declare module DebugProtocol { /** Information about the capabilities of a debug adapter. */ interface Capabilities { - /** The debug adapter supports the 'configurationDone' request. */ + /** The debug adapter supports the `configurationDone` request. */ supportsConfigurationDoneRequest?: boolean; /** The debug adapter supports function breakpoints. */ supportsFunctionBreakpoints?: boolean; @@ -1651,85 +1655,85 @@ declare module DebugProtocol { supportsHitConditionalBreakpoints?: boolean; /** The debug adapter supports a (side effect free) evaluate request for data hovers. */ supportsEvaluateForHovers?: boolean; - /** Available exception filter options for the 'setExceptionBreakpoints' request. */ + /** Available exception filter options for the `setExceptionBreakpoints` request. */ exceptionBreakpointFilters?: ExceptionBreakpointsFilter[]; - /** The debug adapter supports stepping back via the 'stepBack' and 'reverseContinue' requests. */ + /** The debug adapter supports stepping back via the `stepBack` and `reverseContinue` requests. */ supportsStepBack?: boolean; /** The debug adapter supports setting a variable to a value. */ supportsSetVariable?: boolean; /** The debug adapter supports restarting a frame. */ supportsRestartFrame?: boolean; - /** The debug adapter supports the 'gotoTargets' request. */ + /** The debug adapter supports the `gotoTargets` request. */ supportsGotoTargetsRequest?: boolean; - /** The debug adapter supports the 'stepInTargets' request. */ + /** The debug adapter supports the `stepInTargets` request. */ supportsStepInTargetsRequest?: boolean; - /** The debug adapter supports the 'completions' request. */ + /** The debug adapter supports the `completions` request. */ supportsCompletionsRequest?: boolean; - /** The set of characters that should trigger completion in a REPL. If not specified, the UI should assume the '.' character. */ + /** The set of characters that should trigger completion in a REPL. If not specified, the UI should assume the `.` character. */ completionTriggerCharacters?: string[]; - /** The debug adapter supports the 'modules' request. */ + /** The debug adapter supports the `modules` request. */ supportsModulesRequest?: boolean; /** The set of additional module information exposed by the debug adapter. */ additionalModuleColumns?: ColumnDescriptor[]; /** Checksum algorithms supported by the debug adapter. */ supportedChecksumAlgorithms?: ChecksumAlgorithm[]; - /** The debug adapter supports the 'restart' request. In this case a client should not implement 'restart' by terminating and relaunching the adapter but by calling the RestartRequest. */ + /** The debug adapter supports the `restart` request. In this case a client should not implement `restart` by terminating and relaunching the adapter but by calling the RestartRequest. */ supportsRestartRequest?: boolean; - /** The debug adapter supports 'exceptionOptions' on the setExceptionBreakpoints request. */ + /** The debug adapter supports `exceptionOptions` on the setExceptionBreakpoints request. */ supportsExceptionOptions?: boolean; - /** The debug adapter supports a 'format' attribute on the stackTraceRequest, variablesRequest, and evaluateRequest. */ + /** The debug adapter supports a `format` attribute on the stackTraceRequest, variablesRequest, and evaluateRequest. */ supportsValueFormattingOptions?: boolean; - /** The debug adapter supports the 'exceptionInfo' request. */ + /** The debug adapter supports the `exceptionInfo` request. */ supportsExceptionInfoRequest?: boolean; - /** The debug adapter supports the 'terminateDebuggee' attribute on the 'disconnect' request. */ + /** The debug adapter supports the `terminateDebuggee` attribute on the `disconnect` request. */ supportTerminateDebuggee?: boolean; - /** The debug adapter supports the 'suspendDebuggee' attribute on the 'disconnect' request. */ + /** The debug adapter supports the `suspendDebuggee` attribute on the `disconnect` request. */ supportSuspendDebuggee?: boolean; - /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the 'startFrame' and 'levels' arguments and an optional 'totalFrames' result of the 'StackTrace' request are supported. */ + /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the `startFrame` and `levels` arguments and an optional `totalFrames` result of the `StackTrace` request are supported. */ supportsDelayedStackTraceLoading?: boolean; - /** The debug adapter supports the 'loadedSources' request. */ + /** The debug adapter supports the `loadedSources` request. */ supportsLoadedSourcesRequest?: boolean; - /** The debug adapter supports logpoints by interpreting the 'logMessage' attribute of the SourceBreakpoint. */ + /** The debug adapter supports logpoints by interpreting the `logMessage` attribute of the `SourceBreakpoint`. */ supportsLogPoints?: boolean; - /** The debug adapter supports the 'terminateThreads' request. */ + /** The debug adapter supports the `terminateThreads` request. */ supportsTerminateThreadsRequest?: boolean; - /** The debug adapter supports the 'setExpression' request. */ + /** The debug adapter supports the `setExpression` request. */ supportsSetExpression?: boolean; - /** The debug adapter supports the 'terminate' request. */ + /** The debug adapter supports the `terminate` request. */ supportsTerminateRequest?: boolean; /** The debug adapter supports data breakpoints. */ supportsDataBreakpoints?: boolean; - /** The debug adapter supports the 'readMemory' request. */ + /** The debug adapter supports the `readMemory` request. */ supportsReadMemoryRequest?: boolean; - /** The debug adapter supports the 'writeMemory' request. */ + /** The debug adapter supports the `writeMemory` request. */ supportsWriteMemoryRequest?: boolean; - /** The debug adapter supports the 'disassemble' request. */ + /** The debug adapter supports the `disassemble` request. */ supportsDisassembleRequest?: boolean; - /** The debug adapter supports the 'cancel' request. */ + /** The debug adapter supports the `cancel` request. */ supportsCancelRequest?: boolean; - /** The debug adapter supports the 'breakpointLocations' request. */ + /** The debug adapter supports the `breakpointLocations` request. */ supportsBreakpointLocationsRequest?: boolean; - /** The debug adapter supports the 'clipboard' context value in the 'evaluate' request. */ + /** The debug adapter supports the `clipboard` context value in the `evaluate` request. */ supportsClipboardContext?: boolean; - /** The debug adapter supports stepping granularities (argument 'granularity') for the stepping requests. */ + /** The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests. */ supportsSteppingGranularity?: boolean; /** The debug adapter supports adding breakpoints based on instruction references. */ supportsInstructionBreakpoints?: boolean; - /** The debug adapter supports 'filterOptions' as an argument on the 'setExceptionBreakpoints' request. */ + /** The debug adapter supports `filterOptions` as an argument on the `setExceptionBreakpoints` request. */ supportsExceptionFilterOptions?: boolean; - /** The debug adapter supports the 'singleThread' property on the execution requests ('continue', 'next', 'stepIn', 'stepOut', 'reverseContinue', 'stepBack'). */ + /** The debug adapter supports the `singleThread` property on the execution requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`). */ supportsSingleThreadExecutionRequests?: boolean; } /** An ExceptionBreakpointsFilter is shown in the UI as an filter option for configuring how exceptions are dealt with. */ interface ExceptionBreakpointsFilter { - /** The internal ID of the filter option. This value is passed to the 'setExceptionBreakpoints' request. */ + /** The internal ID of the filter option. This value is passed to the `setExceptionBreakpoints` request. */ filter: string; - /** The name of the filter option. This will be shown in the UI. */ + /** The name of the filter option. This is shown in the UI. */ label: string; /** An optional help text providing additional information about the exception filter. This string is typically shown as a hover and must be translated. */ description?: string; - /** Initial value of the filter option. If not specified a value 'false' is assumed. */ + /** Initial value of the filter option. If not specified a value `false` is assumed. */ default?: boolean; /** Controls whether a condition can be specified for this filter option. If false or missing, a condition can not be set. */ supportsCondition?: boolean; @@ -1741,7 +1745,7 @@ declare module DebugProtocol { interface Message { /** Unique identifier for the message. */ id: number; - /** A format string for the message. Embedded variables have the form '{name}'. + /** A format string for the message. Embedded variables have the form `{name}`. If variable name starts with an underscore character, the variable does not contain user data (PII) and can be safely used for telemetry purposes. */ format: string; @@ -1761,7 +1765,7 @@ declare module DebugProtocol { Two attributes are mandatory: an id identifies a module in the modules view and is used in a ModuleEvent for identifying a module for adding, updating or deleting. The name is used to minimally render the module in the UI. - Additional attributes can be added to the module. They will show up in the module View if they have a corresponding ColumnDescriptor. + Additional attributes can be added to the module. They show up in the module View if they have a corresponding ColumnDescriptor. To avoid an unnecessary proliferation of additional attributes with similar semantics but different names, we recommend to re-use attributes from the 'recommended' list below first, and only introduce new attributes if nothing appropriate could be found. */ @@ -1770,11 +1774,7 @@ declare module DebugProtocol { id: number | string; /** A name of the module. */ name: string; - /** optional but recommended attributes. - always try to use these first before introducing additional attributes. - - Logical full path to the module. The exact definition is implementation defined, but usually this would be a full path to the on-disk file for the module. - */ + /** Logical full path to the module. The exact definition is implementation defined, but usually this would be a full path to the on-disk file for the module. */ path?: string; /** True if the module is optimized. */ isOptimized?: boolean; @@ -1786,7 +1786,7 @@ declare module DebugProtocol { symbolStatus?: string; /** Logical full path to the symbol file. The exact definition is implementation defined. */ symbolFilePath?: string; - /** Module created or modified. */ + /** Module created or modified, encoded as a RFC 3339 timestamp. */ dateTimeStamp?: string; /** Address range covered by this module. */ addressRange?: string; @@ -1803,7 +1803,7 @@ declare module DebugProtocol { label: string; /** Format to use for the rendered values in this column. TBD how the format strings looks like. */ format?: string; - /** Datatype of values in this column. Defaults to 'string' if not specified. */ + /** Datatype of values in this column. Defaults to `string` if not specified. */ type?: 'string' | 'number' | 'boolean' | 'unixTimestampUTC'; /** Width of this column in characters (hint only). */ width?: number; @@ -1842,10 +1842,10 @@ declare module DebugProtocol { */ sourceReference?: number; /** An optional hint for how to present the source in the UI. - A value of 'deemphasize' can be used to indicate that the source is not available or that it is skipped on stepping. + A value of `deemphasize` can be used to indicate that the source is not available or that it is skipped on stepping. */ presentationHint?: 'normal' | 'emphasize' | 'deemphasize'; - /** The (optional) origin of this source: possible values 'internal module', 'inlined content from source map', etc. */ + /** The optional origin of this source. For example, 'internal module', 'inlined content from source map', etc. */ origin?: string; /** An optional list of sources that are related to this source. These may be the source that generated this source. */ sources?: Source[]; @@ -1860,7 +1860,7 @@ declare module DebugProtocol { /** A Stackframe contains the source location. */ interface StackFrame { /** An identifier for the stack frame. It must be unique across all threads. - This id can be used to retrieve the scopes of the frame with the 'scopesRequest' or to restart the execution of a stackframe. + This id can be used to retrieve the scopes of the frame with the `scopes` request or to restart the execution of a stackframe. */ id: number; /** The name of the stack frame, typically a method name. */ @@ -1875,14 +1875,14 @@ declare module DebugProtocol { endLine?: number; /** An optional end column of the range covered by the stack frame. */ endColumn?: number; - /** Indicates whether this frame can be restarted with the 'restart' request. Clients should only use this if the debug adapter supports the 'restart' request (capability 'supportsRestartRequest' is true). */ + /** Indicates whether this frame can be restarted with the `restart` request. Clients should only use this if the debug adapter supports the `restart` request and the corresponding capability `supportsRestartRequest` is true. */ canRestart?: boolean; /** Optional memory reference for the current instruction pointer in this frame. */ instructionPointerReference?: string; /** The module associated with this frame, if any. */ moduleId?: number | string; /** An optional hint for how to present this frame in the UI. - A value of 'label' can be used to indicate that the frame is an artificial frame that is used as a visual label or separator. A value of 'subtle' can be used to change the appearance of a frame in a 'subtle' way. + A value of `label` can be used to indicate that the frame is an artificial frame that is used as a visual label or separator. A value of `subtle` can be used to change the appearance of a frame in a 'subtle' way. */ presentationHint?: 'normal' | 'label' | 'subtle'; } @@ -1895,7 +1895,7 @@ declare module DebugProtocol { Values: 'arguments': Scope contains method arguments. 'locals': Scope contains local variables. - 'registers': Scope contains registers. Only a single 'registers' scope should be returned from a 'scopes' request. + 'registers': Scope contains registers. Only a single `registers` scope should be returned from a `scopes` request. etc. */ presentationHint?: 'arguments' | 'locals' | 'registers' | string; @@ -1924,10 +1924,10 @@ declare module DebugProtocol { } /** A Variable is a name/value pair. - Optionally a variable can have a 'type' that is shown if space permits or when hovering over the variable's name. - An optional 'kind' is used to render additional properties of the variable, e.g. different icons can be used to indicate that a variable is public or private. + Optionally a variable can have a `type` that is shown if space permits or when hovering over the variable's name. + An optional `kind` is used to render additional properties of the variable, e.g. different icons can be used to indicate that a variable is public or private. If the value is structured (has children), a handle is provided to retrieve the children with the VariablesRequest. - If the number of named or indexed children is large, the numbers should be returned via the optional 'namedVariables' and 'indexedVariables' attributes. + If the number of named or indexed children is large, the numbers should be returned via the optional `namedVariables` and `indexedVariables` attributes. The client can use this optional information to present the children in a paged UI and fetch them in chunks. */ interface Variable { @@ -1940,12 +1940,12 @@ declare module DebugProtocol { */ value: string; /** The type of the variable's value. Typically shown in the UI when hovering over the value. - This attribute should only be returned by a debug adapter if the client has passed the value true for the 'supportsVariableType' capability of the 'initialize' request. + This attribute should only be returned by a debug adapter if the corresponding capability `supportsVariableType` is true. */ type?: string; /** Properties of a variable that can be used to determine how to render the variable in the UI. */ presentationHint?: VariablePresentationHint; - /** Optional evaluatable name of this variable which can be passed to the 'EvaluateRequest' to fetch the variable's value. */ + /** Optional evaluatable name of this variable which can be passed to the `evaluate` request to fetch the variable's value. */ evaluateName?: string; /** If variablesReference is > 0, the variable is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. */ variablesReference: number; @@ -1958,7 +1958,7 @@ declare module DebugProtocol { */ indexedVariables?: number; /** Optional memory reference for the variable if the variable represents executable code, such as a function pointer. - This attribute is only required if the client has passed the value true for the 'supportsMemoryReferences' capability of the 'initialize' request. + This attribute is only required if the corresponding capability `supportsMemoryReferences` is true. */ memoryReference?: string; } @@ -1977,7 +1977,7 @@ declare module DebugProtocol { 'interface': Indicates that the object is an interface. 'mostDerivedClass': Indicates that the object is the most derived class. 'virtual': Indicates that the object is virtual, that means it is a synthetic object introduced by the adapter for rendering purposes, e.g. an index range for large arrays. - 'dataBreakpoint': Deprecated: Indicates that a data breakpoint is registered for the object. The 'hasDataBreakpoint' attribute should generally be used instead. + 'dataBreakpoint': Deprecated: Indicates that a data breakpoint is registered for the object. The `hasDataBreakpoint` attribute should generally be used instead. etc. */ kind?: 'property' | 'method' | 'class' | 'data' | 'event' | 'baseClass' | 'innerClass' | 'interface' | 'mostDerivedClass' | 'virtual' | 'dataBreakpoint' | string; @@ -2000,12 +2000,12 @@ declare module DebugProtocol { visibility?: 'public' | 'private' | 'protected' | 'internal' | 'final' | string; /** If true, clients can present the variable with a UI that supports a specific gesture to trigger its evaluation. This mechanism can be used for properties that require executing code when retrieving their value and where the code execution can be expensive and/or produce side-effects. A typical example are properties based on a getter function. - Please note that in addition to the 'lazy' flag, the variable's 'variablesReference' must refer to a variable that will provide the value through another 'variable' request. + Please note that in addition to the `lazy` flag, the variable's `variablesReference` must refer to a variable that will provide the value through another `variable` request. */ lazy?: boolean; } - /** Properties of a breakpoint location returned from the 'breakpointLocations' request. */ + /** Properties of a breakpoint location returned from the `breakpointLocations` request. */ interface BreakpointLocation { /** Start line of breakpoint location. */ line: number; @@ -2024,17 +2024,17 @@ declare module DebugProtocol { /** An optional source column of the breakpoint. */ column?: number; /** An optional expression for conditional breakpoints. - It is only honored by a debug adapter if the capability 'supportsConditionalBreakpoints' is true. + It is only honored by a debug adapter if the corresponding capability `supportsConditionalBreakpoints` is true. */ condition?: string; /** An optional expression that controls how many hits of the breakpoint are ignored. The debug adapter is expected to interpret the expression as needed. - The attribute is only honored by a debug adapter if the capability 'supportsHitConditionalBreakpoints' is true. + The attribute is only honored by a debug adapter if the corresponding capability `supportsHitConditionalBreakpoints` is true. */ hitCondition?: string; /** If this attribute exists and is non-empty, the debug adapter must not 'break' (stop) - but log the message instead. Expressions within {} are interpolated. - The attribute is only honored by a debug adapter if the capability 'supportsLogPoints' is true. + but log the message instead. Expressions within `{}` are interpolated. + The attribute is only honored by a debug adapter if the corresponding capability `supportsLogPoints` is true. */ logMessage?: string; } @@ -2044,12 +2044,12 @@ declare module DebugProtocol { /** The name of the function. */ name: string; /** An optional expression for conditional breakpoints. - It is only honored by a debug adapter if the capability 'supportsConditionalBreakpoints' is true. + It is only honored by a debug adapter if the corresponding capability `supportsConditionalBreakpoints` is true. */ condition?: string; /** An optional expression that controls how many hits of the breakpoint are ignored. The debug adapter is expected to interpret the expression as needed. - The attribute is only honored by a debug adapter if the capability 'supportsHitConditionalBreakpoints' is true. + The attribute is only honored by a debug adapter if the corresponding capability `supportsHitConditionalBreakpoints` is true. */ hitCondition?: string; } @@ -2082,12 +2082,12 @@ declare module DebugProtocol { */ offset?: number; /** An optional expression for conditional breakpoints. - It is only honored by a debug adapter if the capability 'supportsConditionalBreakpoints' is true. + It is only honored by a debug adapter if the corresponding capability `supportsConditionalBreakpoints` is true. */ condition?: string; /** An optional expression that controls how many hits of the breakpoint are ignored. The debug adapter is expected to interpret the expression as needed. - The attribute is only honored by a debug adapter if the capability 'supportsHitConditionalBreakpoints' is true. + The attribute is only honored by a debug adapter if the corresponding capability `supportsHitConditionalBreakpoints` is true. */ hitCondition?: string; } @@ -2122,16 +2122,16 @@ declare module DebugProtocol { offset?: number; } - /** The granularity of one 'step' in the stepping requests 'next', 'stepIn', 'stepOut', and 'stepBack'. + /** The granularity of one 'step' in the stepping requests `next`, `stepIn`, `stepOut`, and `stepBack`. 'statement': The step should allow the program to run until the current statement has finished executing. The meaning of a statement is determined by the adapter and it may be considered equivalent to a line. - For example 'for(int i = 0; i < 10; i++) could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'. + For example 'for(int i = 0; i < 10; i++)' could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'. 'line': The step should allow the program to run until the current source line has executed. 'instruction': The step should allow one instruction to execute (e.g. one x86 instruction). */ type SteppingGranularity = 'statement' | 'line' | 'instruction'; - /** A StepInTarget can be used in the 'stepIn' request and determines into which single target the stepIn request should step. */ + /** A StepInTarget can be used in the `stepIn` request and determines into which single target the stepIn request should step. */ interface StepInTarget { /** Unique identifier for a stepIn target. */ id: number; @@ -2147,8 +2147,8 @@ declare module DebugProtocol { endColumn?: number; } - /** A GotoTarget describes a code location that can be used as a target in the 'goto' request. - The possible goto targets can be determined via the 'gotoTargets' request. + /** A GotoTarget describes a code location that can be used as a target in the `goto` request. + The possible goto targets can be determined via the `gotoTargets` request. */ interface GotoTarget { /** Unique identifier for a goto target. This is used in the goto request. */ @@ -2171,16 +2171,16 @@ declare module DebugProtocol { interface CompletionItem { /** The label of this completion item. By default this is also the text that is inserted when selecting this completion. */ label: string; - /** If text is not falsy then it is inserted instead of the label. */ + /** If text is returned and not an empty string, then it is inserted instead of the label. */ text?: string; - /** A string that should be used when comparing this item with other items. When `falsy` the label is used. */ + /** A string that should be used when comparing this item with other items. If not returned or an empty string, the `label` is used instead. */ sortText?: string; /** A human-readable string with additional information about this item, like type or symbol information. */ detail?: string; /** The item's type. Typically the client uses this information to render the item in the UI with an icon. */ type?: CompletionItemType; - /** This value determines the location (in the CompletionsRequest's 'text' attribute) where the completion text is added. - If missing the text is added at the location specified by the CompletionsRequest's 'column' attribute. + /** This value determines the location (in the CompletionsRequest's `text` attribute) where the completion text is added. + If missing the text is added at the location specified by the CompletionsRequest's `column` attribute. */ start?: number; /** This value determines how many characters are overwritten by the completion text. @@ -2209,7 +2209,7 @@ declare module DebugProtocol { interface Checksum { /** The algorithm used to calculate this checksum. */ algorithm: ChecksumAlgorithm; - /** Value of the checksum. */ + /** Value of the checksum, encoded as a hexadecimal value. */ checksum: string; } @@ -2237,19 +2237,19 @@ declare module DebugProtocol { includeAll?: boolean; } - /** An ExceptionFilterOptions is used to specify an exception filter together with a condition for the 'setExceptionBreakpoints' request. */ + /** An ExceptionFilterOptions is used to specify an exception filter together with a condition for the `setExceptionBreakpoints` request. */ interface ExceptionFilterOptions { - /** ID of an exception filter returned by the 'exceptionBreakpointFilters' capability. */ + /** ID of an exception filter returned by the `exceptionBreakpointFilters` capability. */ filterId: string; /** An optional expression for conditional exceptions. - The exception will break into the debugger if the result of the condition is true. + The exception breaks into the debugger if the result of the condition is true. */ condition?: string; } /** An ExceptionOptions assigns configuration options to a set of exceptions. */ interface ExceptionOptions { - /** A path that selects a single or multiple exceptions in a tree. If 'path' is missing, the whole tree is selected. + /** A path that selects a single or multiple exceptions in a tree. If `path` is missing, the whole tree is selected. By convention the first segment of the path is a category that is used to group exceptions in the UI. */ path?: ExceptionPathSegment[]; @@ -2266,12 +2266,12 @@ declare module DebugProtocol { type ExceptionBreakMode = 'never' | 'always' | 'unhandled' | 'userUnhandled'; /** An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in a tree of exceptions. - If a segment consists of more than one name, it matches the names provided if 'negate' is false or missing, or it matches anything except the names provided if 'negate' is true. + If a segment consists of more than one name, it matches the names provided if `negate` is false or missing, or it matches anything except the names provided if `negate` is true. */ interface ExceptionPathSegment { /** If false or missing this segment matches the names provided, otherwise it matches anything except the names provided. */ negate?: boolean; - /** Depending on the value of 'negate' the names that should match or not match. */ + /** Depending on the value of `negate` the names that should match or not match. */ names: string[]; } @@ -2293,7 +2293,7 @@ declare module DebugProtocol { /** Represents a single disassembled instruction. */ interface DisassembledInstruction { - /** The address of the instruction. Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. */ + /** The address of the instruction. Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. */ address: string; /** Optional raw bytes representing the instruction and its operands, in an implementation-defined format. */ instructionBytes?: string; @@ -2316,7 +2316,7 @@ declare module DebugProtocol { endColumn?: number; } - /** Logical areas that can be invalidated by the 'invalidated' event. + /** Logical areas that can be invalidated by the `invalidated` event. Values: 'all': All previously fetched data has become invalid and needs to be refetched. 'stacks': Previously fetched stack related data has become invalid and needs to be refetched. @@ -2326,3 +2326,4 @@ declare module DebugProtocol { */ type InvalidatedAreas = 'all' | 'stacks' | 'threads' | 'variables' | string; } + diff --git a/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts b/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts index 51218a3475d..5e9bb1fa33e 100644 --- a/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts +++ b/src/vs/workbench/contrib/debug/common/loadedScriptsPicker.ts @@ -17,12 +17,10 @@ import { dirname } from 'vs/base/common/resources'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; - -interface IPickerLoadedScriptItem extends IQuickPickItem { +export interface IPickerDebugItem extends IQuickPickItem { accept(): void; } - /** * This function takes a regular quickpick and makes one for loaded scripts that has persistent headers * e.g. when some picks are filtered out, the ones that are visible still have its header. @@ -37,7 +35,7 @@ export async function showLoadedScriptMenu(accessor: ServicesAccessor) { const labelService = accessor.get(ILabelService); const localDisposableStore = new DisposableStore(); - const quickPick = quickInputService.createQuickPick<IPickerLoadedScriptItem>(); + const quickPick = quickInputService.createQuickPick<IPickerDebugItem>(); localDisposableStore.add(quickPick); quickPick.matchOnLabel = quickPick.matchOnDescription = quickPick.matchOnDetail = quickPick.sortByLabel = false; quickPick.placeholder = nls.localize('moveFocusedView.selectView', "Search loaded scripts by name"); @@ -55,8 +53,8 @@ export async function showLoadedScriptMenu(accessor: ServicesAccessor) { quickPick.show(); } -async function _getPicksFromSession(session: IDebugSession, filter: string, editorService: IEditorService, modelService: IModelService, languageService: ILanguageService, labelService: ILabelService): Promise<Array<IPickerLoadedScriptItem | IQuickPickSeparator>> { - const items: Array<IPickerLoadedScriptItem | IQuickPickSeparator> = []; +async function _getPicksFromSession(session: IDebugSession, filter: string, editorService: IEditorService, modelService: IModelService, languageService: ILanguageService, labelService: ILabelService): Promise<Array<IPickerDebugItem | IQuickPickSeparator>> { + const items: Array<IPickerDebugItem | IQuickPickSeparator> = []; items.push({ type: 'separator', label: session.name }); const sources = await session.getLoadedSources(); @@ -69,8 +67,8 @@ async function _getPicksFromSession(session: IDebugSession, filter: string, edit }); return items; } -async function _getPicks(filter: string, sessions: IDebugSession[], editorService: IEditorService, modelService: IModelService, languageService: ILanguageService, labelService: ILabelService): Promise<Array<IPickerLoadedScriptItem | IQuickPickSeparator>> { - const loadedScriptPicks: Array<IPickerLoadedScriptItem | IQuickPickSeparator> = []; +async function _getPicks(filter: string, sessions: IDebugSession[], editorService: IEditorService, modelService: IModelService, languageService: ILanguageService, labelService: ILabelService): Promise<Array<IPickerDebugItem | IQuickPickSeparator>> { + const loadedScriptPicks: Array<IPickerDebugItem | IQuickPickSeparator> = []; const picks = await Promise.all( @@ -85,7 +83,7 @@ async function _getPicks(filter: string, sessions: IDebugSession[], editorServic return loadedScriptPicks; } -function _createPick(source: Source, filter: string, editorService: IEditorService, modelService: IModelService, languageService: ILanguageService, labelService: ILabelService): IPickerLoadedScriptItem | undefined { +function _createPick(source: Source, filter: string, editorService: IEditorService, modelService: IModelService, languageService: ILanguageService, labelService: ILabelService): IPickerDebugItem | undefined { const label = labelService.getUriBasenameLabel(source.uri); const desc = labelService.getUriLabel(dirname(source.uri)); diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 3e9f0e81234..59d8a169b47 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -57,7 +57,7 @@ export async function hasChildProcesses(processId: number | undefined): Promise< const enum ShellType { cmd, powershell, bash } -export function prepareCommand(shell: string, args: string[], cwd?: string, env?: { [key: string]: string | null }): string { +export function prepareCommand(shell: string, args: string[], argsCanBeInterpretedByShell: boolean, cwd?: string, env?: { [key: string]: string | null }): string { shell = shell.trim().toLowerCase(); @@ -109,10 +109,11 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? } } if (args.length > 0) { - const cmd = quote(args.shift()!); + const arg = args.shift()!; + const cmd = argsCanBeInterpretedByShell ? arg : quote(arg); command += (cmd[0] === '\'') ? `& ${cmd} ` : `${cmd} `; for (const a of args) { - command += (a === '<' || a === '>') ? a : quote(a); + command += (a === '<' || a === '>' || argsCanBeInterpretedByShell) ? a : quote(a); command += ' '; } } @@ -150,7 +151,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? } } for (const a of args) { - command += (a === '<' || a === '>') ? a : quote(a); + command += (a === '<' || a === '>' || argsCanBeInterpretedByShell) ? a : quote(a); command += ' '; } if (env) { @@ -185,7 +186,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? command += ' '; } for (const a of args) { - command += (a === '<' || a === '>') ? a : quote(a); + command += (a === '<' || a === '>' || argsCanBeInterpretedByShell) ? a : quote(a); command += ' '; } break; diff --git a/src/vs/workbench/contrib/debug/test/node/terminals.test.ts b/src/vs/workbench/contrib/debug/test/node/terminals.test.ts index 71abb2e2b89..07a16c2245f 100644 --- a/src/vs/workbench/contrib/debug/test/node/terminals.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/terminals.test.ts @@ -10,67 +10,109 @@ import { prepareCommand } from 'vs/workbench/contrib/debug/node/terminals'; suite('Debug - prepareCommand', () => { test('bash', () => { assert.strictEqual( - prepareCommand('bash', ['{$} (']).trim(), + prepareCommand('bash', ['{$} ('], false).trim(), '\\{\\$\\}\\ \\('); assert.strictEqual( - prepareCommand('bash', ['hello', 'world', '--flag=true']).trim(), + prepareCommand('bash', ['hello', 'world', '--flag=true'], false).trim(), 'hello world --flag=true'); assert.strictEqual( - prepareCommand('bash', [' space arg ']).trim(), + prepareCommand('bash', [' space arg '], false).trim(), '\\ space\\ arg\\'); + + assert.strictEqual( + prepareCommand('bash', ['{$} ('], true).trim(), + '{$} ('); + assert.strictEqual( + prepareCommand('bash', ['hello', 'world', '--flag=true'], true).trim(), + 'hello world --flag=true'); + assert.strictEqual( + prepareCommand('bash', [' space arg '], true).trim(), + 'space arg'); }); test('bash - do not escape > and <', () => { assert.strictEqual( - prepareCommand('bash', ['arg1', '>', '> hello.txt', '<', '<input.in']).trim(), + prepareCommand('bash', ['arg1', '>', '> hello.txt', '<', '<input.in'], false).trim(), 'arg1 > \\>\\ hello.txt < \\<input.in'); }); test('cmd', () => { assert.strictEqual( - prepareCommand('cmd.exe', ['^!< ']).trim(), + prepareCommand('cmd.exe', ['^!< '], false).trim(), '"^^^!^< "'); assert.strictEqual( - prepareCommand('cmd.exe', ['hello', 'world', '--flag=true']).trim(), + prepareCommand('cmd.exe', ['hello', 'world', '--flag=true'], false).trim(), 'hello world --flag=true'); assert.strictEqual( - prepareCommand('cmd.exe', [' space arg ']).trim(), + prepareCommand('cmd.exe', [' space arg '], false).trim(), '" space arg "'); assert.strictEqual( - prepareCommand('cmd.exe', ['"A>0"']).trim(), + prepareCommand('cmd.exe', ['"A>0"'], false).trim(), '"""A^>0"""'); assert.strictEqual( - prepareCommand('cmd.exe', ['']).trim(), + prepareCommand('cmd.exe', [''], false).trim(), '""'); + + assert.strictEqual( + prepareCommand('cmd.exe', ['^!< '], true).trim(), + '^!<'); + assert.strictEqual( + prepareCommand('cmd.exe', ['hello', 'world', '--flag=true'], true).trim(), + 'hello world --flag=true'); + assert.strictEqual( + prepareCommand('cmd.exe', [' space arg '], true).trim(), + 'space arg'); + assert.strictEqual( + prepareCommand('cmd.exe', ['"A>0"'], true).trim(), + '"A>0"'); + assert.strictEqual( + prepareCommand('cmd.exe', [''], true).trim(), + ''); }); test('cmd - do not escape > and <', () => { assert.strictEqual( - prepareCommand('cmd.exe', ['arg1', '>', '> hello.txt', '<', '<input.in']).trim(), + prepareCommand('cmd.exe', ['arg1', '>', '> hello.txt', '<', '<input.in'], false).trim(), 'arg1 > "^> hello.txt" < ^<input.in'); }); test('powershell', () => { assert.strictEqual( - prepareCommand('powershell', ['!< ']).trim(), + prepareCommand('powershell', ['!< '], false).trim(), `& '!< '`); assert.strictEqual( - prepareCommand('powershell', ['hello', 'world', '--flag=true']).trim(), + prepareCommand('powershell', ['hello', 'world', '--flag=true'], false).trim(), `& 'hello' 'world' '--flag=true'`); assert.strictEqual( - prepareCommand('powershell', [' space arg ']).trim(), + prepareCommand('powershell', [' space arg '], false).trim(), `& ' space arg '`); assert.strictEqual( - prepareCommand('powershell', ['"A>0"']).trim(), + prepareCommand('powershell', ['"A>0"'], false).trim(), `& '"A>0"'`); assert.strictEqual( - prepareCommand('powershell', ['']).trim(), + prepareCommand('powershell', [''], false).trim(), `& ''`); + + assert.strictEqual( + prepareCommand('powershell', ['!< '], true).trim(), + '!<'); + assert.strictEqual( + prepareCommand('powershell', ['hello', 'world', '--flag=true'], true).trim(), + 'hello world --flag=true'); + assert.strictEqual( + prepareCommand('powershell', [' space arg '], true).trim(), + 'space arg'); + assert.strictEqual( + prepareCommand('powershell', ['"A>0"'], true).trim(), + '"A>0"'); + assert.strictEqual( + prepareCommand('powershell', [''], true).trim(), + ``); }); test('powershell - do not escape > and <', () => { assert.strictEqual( - prepareCommand('powershell', ['arg1', '>', '> hello.txt', '<', '<input.in']).trim(), + prepareCommand('powershell', ['arg1', '>', '> hello.txt', '<', '<input.in'], false).trim(), `& 'arg1' > '> hello.txt' < '<input.in'`); }); }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 6cf90434c7b..b85bfe7485f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -60,8 +60,9 @@ interface InstalledExtensionsEvent { } interface ExtensionsLoadClassification extends GDPRClassification<InstalledExtensionsEvent> { owner: 'digitarald'; - readonly extensionIds: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; - readonly count: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; + comment: 'Helps to understand which extensions are the most actively used.', + readonly extensionIds: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The list of extension ids that are installed.' }; + readonly count: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The number of extensions that are installed.' }; } export class Extension implements IExtension { diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index d3ae1ad9f6b..849c3016594 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -47,7 +47,8 @@ import { hash } from 'vs/base/common/hash'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ViewContainerLocation } from 'vs/workbench/common/views'; -import { OPEN_TO_SIDE_COMMAND_ID, COMPARE_WITH_SAVED_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, COMPARE_SELECTED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, COPY_PATH_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_WITH_EXPLORER_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_AS_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_IN_GROUP_COMMAND_ID, SAVE_FILES_COMMAND_ID, REVERT_FILE_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, PREVIOUS_COMPRESSED_FOLDER, NEXT_COMPRESSED_FOLDER, FIRST_COMPRESSED_FOLDER, LAST_COMPRESSED_FOLDER, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL } from './fileConstants'; +import { OPEN_TO_SIDE_COMMAND_ID, COMPARE_WITH_SAVED_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, COMPARE_SELECTED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, COPY_PATH_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_WITH_EXPLORER_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_AS_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_IN_GROUP_COMMAND_ID, SAVE_FILES_COMMAND_ID, REVERT_FILE_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, PREVIOUS_COMPRESSED_FOLDER, NEXT_COMPRESSED_FOLDER, FIRST_COMPRESSED_FOLDER, LAST_COMPRESSED_FOLDER, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL, NEW_FILE_COMMAND_ID } from './fileConstants'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; export const openWindowCommand = (accessor: ServicesAccessor, toOpen: IWindowOpenable[], options?: IOpenWindowOptions) => { if (Array.isArray(toOpen)) { @@ -639,19 +640,45 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ 'languageId': { 'type': 'string' }, - 'path': { - 'type': 'string' - } } } } ] }, - handler: async (accessor, args?: { languageId?: string; viewType?: string; path?: string }) => { + handler: async (accessor, args?: { languageId?: string; viewType?: string }) => { + const editorService = accessor.get(IEditorService); + + await editorService.openEditor({ + resource: undefined, + options: { + override: args?.viewType, + pinned: true + }, + languageId: args?.languageId, + }); + } +}); + +CommandsRegistry.registerCommand({ + id: NEW_FILE_COMMAND_ID, + handler: async (accessor, args?: { languageId?: string; viewType?: string; fileName?: string }) => { const editorService = accessor.get(IEditorService); + const dialogService = accessor.get(IFileDialogService); + const fileService = accessor.get(IFileService); + + const createFileLocalized = nls.localize('newFileCommand.saveLabel', "Create File"); + const defaultFileUri = joinPath(await dialogService.defaultFilePath(), args?.fileName ?? 'Untitled.txt'); + + const saveUri = await dialogService.showSaveDialog({ saveLabel: createFileLocalized, title: createFileLocalized, defaultUri: defaultFileUri }); + + if (!saveUri) { + return; + } + + await fileService.createFile(saveUri, undefined, { overwrite: true }); await editorService.openEditor({ - resource: args?.path ? URI.from({ scheme: Schemas.untitled, path: args.path }) : undefined, + resource: saveUri, options: { override: args?.viewType, pinned: true diff --git a/src/vs/workbench/contrib/files/browser/fileConstants.ts b/src/vs/workbench/contrib/files/browser/fileConstants.ts index 4e65b120256..4b9ff14454d 100644 --- a/src/vs/workbench/contrib/files/browser/fileConstants.ts +++ b/src/vs/workbench/contrib/files/browser/fileConstants.ts @@ -46,3 +46,4 @@ export const FIRST_COMPRESSED_FOLDER = 'firstCompressedFolder'; export const LAST_COMPRESSED_FOLDER = 'lastCompressedFolder'; export const NEW_UNTITLED_FILE_COMMAND_ID = 'workbench.action.files.newUntitledFile'; export const NEW_UNTITLED_FILE_LABEL = nls.localize('newUntitledFile', "New Untitled File"); +export const NEW_FILE_COMMAND_ID = 'workbench.action.files.newFile'; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index a9cd6b8a8c9..e90d052ae29 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -310,6 +310,8 @@ export class ExplorerView extends ViewPane implements IExplorerView { if (visible) { // Always refresh explorer when it becomes visible to compensate for missing file events #126817 await this.setTreeInput(); + // Update the collapse / expand button state + this.updateAnyCollapsedContext(); // Find resource to focus from active editor input if set this.selectActiveFile(true); } @@ -505,9 +507,10 @@ export class ExplorerView extends ViewPane implements IExplorerView { const navigationController = this.renderer.getCompressedNavigationController(element instanceof Array ? element[0] : element); navigationController?.updateCollapsed(e.node.collapsed); } + // Update showing expand / collapse button + this.updateAnyCollapsedContext(); })); - this._register(this.tree.onDidChangeCollapseState(() => this.updateAnyCollapsedContext())); this.updateAnyCollapsedContext(); this._register(this.tree.onMouseDblClick(e => { diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 9969bd9c645..c6442b9ce9c 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -203,9 +203,11 @@ export class InteractiveDocumentContribution extends Disposable implements IWork canSupportResource: uri => uri.scheme === Schemas.vscodeInteractiveInput, singlePerResource: true }, - ({ resource }) => { - const editorInput = editorService.getEditors(EditorsOrder.SEQUENTIAL).find(editor => editor.editor instanceof InteractiveEditorInput && editor.editor.inputResource.toString() === resource.toString()); - return editorInput!; + { + createEditorInput: ({ resource }) => { + const editorInput = editorService.getEditors(EditorsOrder.SEQUENTIAL).find(editor => editor.editor instanceof InteractiveEditorInput && editor.editor.inputResource.toString() === resource.toString()); + return editorInput!; + } } ); @@ -220,23 +222,25 @@ export class InteractiveDocumentContribution extends Disposable implements IWork canSupportResource: uri => uri.scheme === Schemas.vscodeInteractive || (uri.scheme === Schemas.vscodeNotebookCell && extname(uri) === '.interactive'), singlePerResource: true }, - ({ resource, options }) => { - const data = CellUri.parse(resource); - let notebookUri: URI = resource; - let cellOptions: IResourceEditorInput | undefined; - - if (data) { - notebookUri = data.notebook; - cellOptions = { resource, options }; - } + { + createEditorInput: ({ resource, options }) => { + const data = CellUri.parse(resource); + let notebookUri: URI = resource; + let cellOptions: IResourceEditorInput | undefined; + + if (data) { + notebookUri = data.notebook; + cellOptions = { resource, options }; + } - const notebookOptions = { ...options, cellOptions } as INotebookEditorOptions; + const notebookOptions = { ...options, cellOptions } as INotebookEditorOptions; - const editorInput = editorService.getEditors(EditorsOrder.SEQUENTIAL).find(editor => editor.editor instanceof InteractiveEditorInput && editor.editor.resource?.toString() === notebookUri.toString()); - return { - editor: editorInput!.editor, - options: notebookOptions - }; + const editorInput = editorService.getEditors(EditorsOrder.SEQUENTIAL).find(editor => editor.editor instanceof InteractiveEditorInput && editor.editor.resource?.toString() === notebookUri.toString()); + return { + editor: editorInput!.editor, + options: notebookOptions + }; + } } ); } diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 1652365c097..778ae2c5497 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -19,7 +19,7 @@ import { IDisposable, dispose, Disposable, toDisposable, DisposableStore } from import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { QuickFixAction, QuickFixActionViewItem } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import { ILabelService } from 'vs/platform/label/common/label'; -import { dirname, basename, isEqual } from 'vs/base/common/resources'; +import { basename, isEqual } from 'vs/base/common/resources'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeFilter, TreeVisibility, TreeFilterResult, ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { FilterOptions } from 'vs/workbench/contrib/markers/browser/markersFilterOptions'; @@ -185,7 +185,7 @@ export class ResourceMarkersRenderer implements ITreeRenderer<ResourceMarkers, R if (this.fileService.hasProvider(resourceMarkers.resource) || resourceMarkers.resource.scheme === network.Schemas.untitled) { templateData.resourceLabel.setFile(resourceMarkers.resource, { matches: uriMatches }); } else { - templateData.resourceLabel.setResource({ name: resourceMarkers.name, description: this.labelService.getUriLabel(dirname(resourceMarkers.resource), { relative: true }), resource: resourceMarkers.resource }, { matches: uriMatches }); + templateData.resourceLabel.setResource({ name: resourceMarkers.name, description: this.labelService.getUriLabel(resourceMarkers.resource, { relative: true }), resource: resourceMarkers.resource }, { matches: uriMatches }); } this.updateCount(node, templateData); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts index 2a0d776fb18..3c028ec8ebd 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts @@ -9,9 +9,10 @@ import { localize } from 'vs/nls'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { Action2, MenuId } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { IResourceMergeEditorInput } from 'vs/workbench/common/editor'; import { MergeEditorInput, MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; import { ctxIsMergeEditor, ctxMergeEditorLayout } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; @@ -27,15 +28,14 @@ export class OpenMergeEditor extends Action2 { run(accessor: ServicesAccessor, ...args: unknown[]): void { const validatedArgs = IRelaxedOpenArgs.validate(args[0]); - const instaService = accessor.get(IInstantiationService); - const input = instaService.createInstance( - MergeEditorInput, - validatedArgs.base, - validatedArgs.input1, - validatedArgs.input2, - validatedArgs.output, - ); - accessor.get(IEditorService).openEditor(input, { preserveFocus: true }); + const input: IResourceMergeEditorInput = { + base: { resource: validatedArgs.base }, + input1: { resource: validatedArgs.input1.uri, label: validatedArgs.input1.title, description: validatedArgs.input1.description, detail: validatedArgs.input1.detail }, + input2: { resource: validatedArgs.input2.uri, label: validatedArgs.input2.title, description: validatedArgs.input2.description, detail: validatedArgs.input2.detail }, + result: { resource: validatedArgs.output }, + options: { preserveFocus: true } + }; + accessor.get(IEditorService).openEditor(input); } } @@ -188,10 +188,9 @@ export class OpenResultResource extends Action2 { } async run(accessor: ServicesAccessor): Promise<void> { - const opener = accessor.get(IOpenerService); - const { activeEditor } = accessor.get(IEditorService); - if (activeEditor instanceof MergeEditorInput) { - await opener.open(activeEditor.result); + const editorService = accessor.get(IEditorService); + if (editorService.activeEditor instanceof MergeEditorInput) { + editorService.openEditor({ resource: editorService.activeEditor.result }); } } } @@ -331,8 +330,10 @@ export class CompareInput1WithBaseCommand extends Action2 { ), original: 'Compare Input 1 With Base', }, + shortTitle: localize('mergeEditor.compareWithBase', 'Compare With Base'), f1: true, precondition: ctxIsMergeEditor, + menu: { id: MenuId.MergeInput1Toolbar } }); } run(accessor: ServicesAccessor, ...args: unknown[]): void { @@ -354,8 +355,10 @@ export class CompareInput2WithBaseCommand extends Action2 { ), original: 'Compare Input 2 With Base', }, + shortTitle: localize('mergeEditor.compareWithBase', 'Compare With Base'), f1: true, precondition: ctxIsMergeEditor, + menu: { id: MenuId.MergeInput2Toolbar } }); } run(accessor: ServicesAccessor, ...args: unknown[]): void { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts index 6728dc93b1a..0f2b38ea5b4 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/devCommands.ts @@ -10,14 +10,14 @@ import { localize } from 'vs/nls'; import { Action2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files'; import { URI } from 'vs/base/common/uri'; -import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { IResourceMergeEditorInput } from 'vs/workbench/common/editor'; import { ctxIsMergeEditor } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; interface MergeEditorContents { @@ -35,10 +35,10 @@ export class MergeEditorCopyContentsToJSON extends Action2 { category: 'Merge Editor (Dev)', title: { value: localize( - 'merge.dev.copyContents', - 'Copy Contents of Inputs, Base and Result as JSON' + 'merge.dev.copyState', + 'Copy Merge Editor State as JSON' ), - original: 'Copy Contents of Inputs, Base and Result as JSON', + original: 'Copy Merge Editor State as JSON', }, icon: Codicon.layoutCentered, f1: true, @@ -74,7 +74,7 @@ export class MergeEditorCopyContentsToJSON extends Action2 { notificationService.info({ name: localize('mergeEditor.name', 'Merge Editor'), - message: localize('mergeEditor.successfullyCopiedMergeEditorContents', "Successfully copied merge editor contents"), + message: localize('mergeEditor.successfullyCopiedMergeEditorContents', "Successfully copied merge editor state"), }); } } @@ -86,10 +86,10 @@ export class MergeEditorOpenContents extends Action2 { category: 'Merge Editor (Dev)', title: { value: localize( - 'merge.dev.openContents', - 'Open Contents of Inputs, Base and Result from JSON' + 'merge.dev.openState', + 'Open Merge Editor State from JSON' ), - original: 'Open Contents of Inputs, Base and Result from JSON', + original: 'Open Merge Editor State from JSON', }, icon: Codicon.layoutCentered, f1: true, @@ -98,8 +98,6 @@ export class MergeEditorOpenContents extends Action2 { async run(accessor: ServicesAccessor): Promise<void> { const service = accessor.get(IWorkbenchFileService); - const instaService = accessor.get(IInstantiationService); - const editorService = accessor.get(IEditorService); const inputService = accessor.get(IQuickInputService); const clipboardService = accessor.get(IClipboardService); const textModelService = accessor.get(ITextModelService); @@ -153,13 +151,12 @@ export class MergeEditorOpenContents extends Action2 { setLanguageId(resultUri, content.languageId), ]); - const input = instaService.createInstance( - MergeEditorInput, - baseUri, - { uri: input1Uri, title: 'Input 1', description: 'Input 1', detail: '(from JSON)' }, - { uri: input2Uri, title: 'Input 2', description: 'Input 2', detail: '(from JSON)' }, - resultUri, - ); - editorService.openEditor(input); + const input: IResourceMergeEditorInput = { + base: { resource: baseUri }, + input1: { resource: input1Uri, label: 'Input 1', description: 'Input 1', detail: '(from JSON)' }, + input2: { resource: input2Uri, label: 'Input 2', description: 'Input 2', detail: '(from JSON)' }, + result: { resource: resultUri }, + }; + accessor.get(IEditorService).openEditor(input); } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index 9a4286f26b6..50470d8df66 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -140,8 +140,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements override toUntyped(): IResourceMergeEditorInput { return { - input1: { resource: this.input1.uri, label: this.input1.title, description: this.input1.description }, - input2: { resource: this.input2.uri, label: this.input2.title, description: this.input2.description }, + input1: { resource: this.input1.uri, label: this.input1.title, description: this.input1.description, detail: this.input1.detail }, + input2: { resource: this.input2.uri, label: this.input2.title, description: this.input2.description, detail: this.input2.detail }, base: { resource: this.base }, result: { resource: this.result }, options: { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index 321d1594dd5..5ca3ec0e935 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -24,14 +24,15 @@ export abstract class CodeEditorView extends Disposable { readonly model = this._viewModel.map(m => /** @description model */ m?.model); protected readonly htmlElements = h('div.code-view', [ - h('div.title', { $: 'header' }, [ - h('span.title', { $: 'title' }), - h('span.description', { $: 'description' }), - h('span.detail', { $: 'detail' }), + h('div.title@header', [ + h('span.title@title'), + h('span.description@description'), + h('span.detail@detail'), + h('span.toolbar@toolbar'), ]), h('div.container', [ - h('div.gutter', { $: 'gutterDiv' }), - h('div', { $: 'editor' }), + h('div.gutter@gutterDiv'), + h('div@editor'), ]), ]); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts index abadef681ba..14e739314c3 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts @@ -5,6 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -15,6 +16,9 @@ import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/edi import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model'; import { CodeLensContribution } from 'vs/editor/contrib/codelens/browser/codelensController'; import { localize } from 'vs/nls'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { attachToggleStyler } from 'vs/platform/theme/common/styler'; @@ -231,9 +235,12 @@ export class InputCodeEditorView extends CodeEditorView { constructor( public readonly inputNumber: 1 | 2, + titleMenuId: MenuId, @IInstantiationService instantiationService: IInstantiationService, @IContextMenuService contextMenuService: IContextMenuService, @IThemeService themeService: IThemeService, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, ) { super(instantiationService); @@ -247,6 +254,19 @@ export class InputCodeEditorView extends CodeEditorView { createView: (item, target) => new MergeConflictGutterItemView(item, target, contextMenuService, themeService), }) ); + + // title menu + const titleMenu = menuService.createMenu(titleMenuId, contextKeyService); + const toolBar = new ToolBar(this.htmlElements.toolbar, contextMenuService); + const toolBarUpdate = () => { + const secondary: IAction[] = []; + createAndFillInActionBarActions(titleMenu, { renderShortTitle: true }, secondary); + toolBar.setActions([], secondary); + }; + this._store.add(toolBar); + this._store.add(titleMenu); + this._store.add(titleMenu.onDidChange(toolBarUpdate)); + toolBarUpdate(); } protected override getEditorContributions(): IEditorContributionDescription[] | undefined { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/media/mergeEditor.css b/src/vs/workbench/contrib/mergeEditor/browser/view/media/mergeEditor.css index e9c0ada7df3..e8141942cc9 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/media/mergeEditor.css +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/media/mergeEditor.css @@ -31,6 +31,10 @@ flex-shrink: 0; } +.monaco-workbench .merge-editor .code-view > .title>SPAN.detail .codicon { + font-size: 13px; +} + .monaco-workbench .merge-editor .code-view > .container { display: flex; flex-direction: row; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index d98aa657980..46aee59e160 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -11,9 +11,9 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Color } from 'vs/base/common/color'; import { BugIndicatingError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { autorunWithStore, IObservable } from 'vs/base/common/observable'; -import { isEqual } from 'vs/base/common/resources'; +import { basename, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/mergeEditor'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -44,10 +44,10 @@ import { DocumentMapping, getOppositeDirection, MappingDirection } from 'vs/work import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; import { deepMerge, ReentrancyBarrier, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; -import { ctxBaseResourceScheme, ctxIsMergeEditor, ctxMergeEditorLayout, MergeEditorLayoutTypes } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { ctxMergeBaseUri, ctxIsMergeEditor, ctxMergeEditorLayout, ctxMergeResultUri, MergeEditorLayoutTypes } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { IEditorResolverService, MergeEditorInputFactoryFunction, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import './colors'; import { InputCodeEditorView } from './editors/inputCodeEditorView'; @@ -87,14 +87,15 @@ export class MergeEditor extends AbstractTextEditor<IMergeEditorViewState> { private readonly _sessionDisposables = new DisposableStore(); private _grid!: Grid<IView>; - private readonly input1View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 1)); - private readonly input2View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 2)); + private readonly input1View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 1, MenuId.MergeInput1Toolbar)); + private readonly input2View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 2, MenuId.MergeInput2Toolbar)); private readonly inputResultView = this._register(this.instantiationService.createInstance(ResultCodeEditorView)); private readonly _layoutMode: MergeEditorLayout; private readonly _ctxIsMergeEditor: IContextKey<boolean>; private readonly _ctxUsesColumnLayout: IContextKey<string>; - private readonly _ctxBaseResourceScheme: IContextKey<string>; + private readonly _ctxResultUri: IContextKey<string>; + private readonly _ctxBaseUri: IContextKey<string>; private _model: MergeEditorModel | undefined; public get model(): MergeEditorModel | undefined { return this._model; } @@ -121,7 +122,8 @@ export class MergeEditor extends AbstractTextEditor<IMergeEditorViewState> { this._ctxIsMergeEditor = ctxIsMergeEditor.bindTo(_contextKeyService); this._ctxUsesColumnLayout = ctxMergeEditorLayout.bindTo(_contextKeyService); - this._ctxBaseResourceScheme = ctxBaseResourceScheme.bindTo(_contextKeyService); + this._ctxBaseUri = ctxMergeBaseUri.bindTo(_contextKeyService); + this._ctxResultUri = ctxMergeResultUri.bindTo(_contextKeyService); this._layoutMode = instantiation.createInstance(MergeEditorLayout); this._ctxUsesColumnLayout.set(this._layoutMode.value); @@ -205,6 +207,7 @@ export class MergeEditor extends AbstractTextEditor<IMergeEditorViewState> { override dispose(): void { this._sessionDisposables.dispose(); this._ctxIsMergeEditor.reset(); + this._ctxUsesColumnLayout.reset(); super.dispose(); } @@ -305,7 +308,14 @@ export class MergeEditor extends AbstractTextEditor<IMergeEditorViewState> { this.input1View.setModel(viewModel, model.input1, model.input1Title || localize('input1', 'Input 1'), model.input1Detail, model.input1Description); this.input2View.setModel(viewModel, model.input2, model.input2Title || localize('input2', 'Input 2',), model.input2Detail, model.input2Description); this.inputResultView.setModel(viewModel, model.result, localize('result', 'Result',), this._labelService.getUriLabel(model.result.uri, { relative: true }), undefined); - this._ctxBaseResourceScheme.set(model.base.uri.scheme); + + // Set/unset context keys based on input + this._ctxResultUri.set(model.result.uri.toString()); + this._ctxBaseUri.set(model.base.uri.toString()); + this._sessionDisposables.add(toDisposable(() => { + this._ctxBaseUri.reset(); + this._ctxResultUri.reset(); + })); const viewState = this.loadEditorViewState(input, context); if (viewState) { @@ -359,6 +369,45 @@ export class MergeEditor extends AbstractTextEditor<IMergeEditorViewState> { } }); }, 'update alignment view zones')); + + + // detect when base, input1, and input2 become empty and replace THIS editor with its result editor + // TODO@jrieken@hediet this needs a better/cleaner solution + // https://github.com/microsoft/vscode/issues/155940 + const that = this; + this._sessionDisposables.add(new class { + + private readonly _disposable = new DisposableStore(); + + constructor() { + for (const model of this.baseInput1Input2()) { + this._disposable.add(model.onDidChangeContent(() => this._checkBaseInput1Input2AllEmpty())); + } + } + + dispose() { + this._disposable.dispose(); + } + + private *baseInput1Input2() { + yield model.base; + yield model.input1; + yield model.input2; + } + + private _checkBaseInput1Input2AllEmpty() { + for (const model of this.baseInput1Input2()) { + if (model.getValueLength() > 0) { + return; + } + } + // all empty -> replace this editor with a normal editor for result + that.editorService.replaceEditors( + [{ editor: input, replacement: { resource: input.result }, forceReplaceDirty: true }], + that.group ?? that.editorGroupService.activeGroup + ); + } + }); } override setOptions(options: ITextEditorOptions | undefined): void { @@ -510,58 +559,39 @@ export class MergeEditorResolverContribution extends Disposable { ) { super(); + const mergeEditorInputFactory: MergeEditorInputFactoryFunction = (mergeEditor: IResourceMergeEditorInput): EditorInputWithOptions => { + return { + editor: instantiationService.createInstance( + MergeEditorInput, + mergeEditor.base.resource, + { + uri: mergeEditor.input1.resource, + title: mergeEditor.input1.label ?? basename(mergeEditor.input1.resource), + description: mergeEditor.input1.description ?? '', + detail: mergeEditor.input1.detail + }, + { + uri: mergeEditor.input2.resource, + title: mergeEditor.input2.label ?? basename(mergeEditor.input2.resource), + description: mergeEditor.input2.description ?? '', + detail: mergeEditor.input2.detail + }, + mergeEditor.result.resource + ) + }; + }; + this._register(editorResolverService.registerEditor( `*`, { - id: MergeEditorInput.ID, - label: localize('editor.mergeEditor.label', "Merge Editor"), + id: DEFAULT_EDITOR_ASSOCIATION.id, + label: DEFAULT_EDITOR_ASSOCIATION.displayName, detail: DEFAULT_EDITOR_ASSOCIATION.providerDisplayName, - priority: RegisteredEditorPriority.option + priority: RegisteredEditorPriority.builtin }, {}, - (editor) => { - return { - editor: instantiationService.createInstance( - MergeEditorInput, - editor.resource, - { - uri: editor.resource, - title: '', - description: '', - detail: '' - }, - { - uri: editor.resource, - title: '', - description: '', - detail: '' - }, - editor.resource - ) - }; - }, - undefined, - undefined, - (mergeEditor: IResourceMergeEditorInput): EditorInputWithOptions => { - return { - editor: instantiationService.createInstance( - MergeEditorInput, - mergeEditor.base.resource, - { - uri: mergeEditor.input1.resource, - title: localize('input1Title', "First Version"), - description: '', - detail: '' - }, - { - uri: mergeEditor.input2.resource, - title: localize('input2Title', "Second Version"), - description: '', - detail: '' - }, - mergeEditor.result.resource - ) - }; + { + createMergeEditorInput: mergeEditorInputFactory } )); } diff --git a/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts index 9ec46266ae2..e5757fcc8fa 100644 --- a/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export type MergeEditorLayoutTypes = 'mixed' | 'columns'; -export const ctxIsMergeEditor = new RawContextKey<boolean>('isMergeEditor', false); -export const ctxMergeEditorLayout = new RawContextKey<MergeEditorLayoutTypes>('mergeEditorLayout', 'mixed'); -export const ctxBaseResourceScheme = new RawContextKey<string>('baseResourceScheme', ''); +export const ctxIsMergeEditor = new RawContextKey<boolean>('isMergeEditor', false, { type: 'boolean', description: localize('is', 'The editor is a merge editor') }); +export const ctxMergeEditorLayout = new RawContextKey<MergeEditorLayoutTypes>('mergeEditorLayout', 'mixed', { type: 'string', description: localize('editorLayout', 'The layout mode of a merge editor') }); +export const ctxMergeBaseUri = new RawContextKey<string>('mergeEditorBaseUri', '', { type: 'string', description: localize('baseUri', 'The uri of the baser of a merge editor') }); +export const ctxMergeResultUri = new RawContextKey<string>('mergeEditorResultUri', '', { type: 'string', description: localize('resultUri', 'The uri of the result of a merge editor') }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts index 47d41741736..8acf11d042a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts @@ -93,7 +93,8 @@ class ExecutionStateCellStatusBarItem extends Disposable { private _currentItemIds: string[] = []; - private _currentExecutingStateTimer: IDisposable | undefined; + private _showedExecutingStateTime: number | undefined; + private _clearExecutingStateTimer: IDisposable | undefined; constructor( private readonly _notebookViewModel: INotebookViewModel, @@ -123,23 +124,27 @@ class ExecutionStateCellStatusBarItem extends Disposable { */ private _getItemsForCell(): INotebookCellStatusBarItem[] | undefined { const runState = this._executionStateService.getCellExecution(this._cell.uri); - if (this._currentExecutingStateTimer && !runState?.isPaused) { - return; - } - - const item = this._getItemForState(runState, this._cell.internalMetadata); - // Show the execution spinner for a minimum time - if (runState?.state === NotebookCellExecutionState.Executing) { - this._currentExecutingStateTimer = this._register(disposableTimeout(() => { - const runState = this._executionStateService.getCellExecution(this._cell.uri); - this._currentExecutingStateTimer = undefined; - if (runState?.state !== NotebookCellExecutionState.Executing) { - this._update(); + if (runState?.state === NotebookCellExecutionState.Executing && typeof this._showedExecutingStateTime !== 'number') { + this._showedExecutingStateTime = Date.now(); + } else if (runState?.state !== NotebookCellExecutionState.Executing && typeof this._showedExecutingStateTime === 'number') { + const timeUntilMin = ExecutionStateCellStatusBarItem.MIN_SPINNER_TIME - (Date.now() - this._showedExecutingStateTime); + if (timeUntilMin > 0) { + if (!this._clearExecutingStateTimer) { + this._clearExecutingStateTimer = disposableTimeout(() => { + this._showedExecutingStateTime = undefined; + this._clearExecutingStateTimer = undefined; + this._update(); + }, timeUntilMin); } - }, ExecutionStateCellStatusBarItem.MIN_SPINNER_TIME)); + + return undefined; + } else { + this._showedExecutingStateTime = undefined; + } } + const item = this._getItemForState(runState, this._cell.internalMetadata); return item ? [item] : []; } @@ -203,12 +208,16 @@ export class TimerCellStatusBarContrib extends Disposable implements INotebookEd } registerNotebookContribution(TimerCellStatusBarContrib.id, TimerCellStatusBarContrib); +const UPDATE_TIMER_GRACE_PERIOD = 200; + class TimerCellStatusBarItem extends Disposable { private static UPDATE_INTERVAL = 100; private _currentItemIds: string[] = []; private _scheduler: RunOnceScheduler; + private _deferredUpdate: IDisposable | undefined; + constructor( private readonly _notebookViewModel: INotebookViewModel, private readonly _cell: ICellViewModel, @@ -243,7 +252,18 @@ class TimerCellStatusBarItem extends Disposable { } const items = item ? [item] : []; - this._currentItemIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); + if (!items.length && !!runState) { + if (!this._deferredUpdate) { + this._deferredUpdate = disposableTimeout(() => { + this._deferredUpdate = undefined; + this._currentItemIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); + }, UPDATE_TIMER_GRACE_PERIOD); + } + } else { + this._deferredUpdate?.dispose(); + this._deferredUpdate = undefined; + this._currentItemIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); + } } private _getTimeItem(startTime: number, endTime: number, adjustment: number = 0): INotebookCellStatusBarItem { @@ -258,6 +278,7 @@ class TimerCellStatusBarItem extends Disposable { override dispose() { super.dispose(); + this._deferredUpdate?.dispose(); this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items: [] }]); } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts index e98ae9b79f1..975e3bd016a 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -310,8 +310,8 @@ export class NotebookTextDiffList extends WorkbenchList<DiffElementViewModelBase @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService) { - super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); + @IInstantiationService instantiationService: IInstantiationService) { + super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, instantiationService); } protected override createMouseController(options: IListOptions<DiffElementViewModelBase>): MouseController<DiffElementViewModelBase> { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index a3f275365dd..fc6500ceba8 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -1650,6 +1650,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD content: model.getText(), offset: offset, visible: false, + metadata: model.metadata, }); } @@ -2587,6 +2588,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD content: cell.getText(), offset: cellTop + top, visible: true, + metadata: cell.metadata, }); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts index 0cf3e56c598..845cc72df8e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl.ts @@ -124,6 +124,7 @@ export class NotebookExecutionStateService extends Disposable implements INotebo if (!exe) { exe = this._createNotebookCellExecution(notebook, cellHandle); notebookExecutionMap.set(cellHandle, exe); + exe.initialize(); this._onDidChangeCellExecution.fire(new NotebookExecutionEvent(notebookUri, cellHandle, exe)); } @@ -305,6 +306,9 @@ class CellExecution extends Disposable implements INotebookCellExecution { ) { super(); this._logService.debug(`CellExecution#ctor ${this.getCellLog()}`); + } + + initialize() { const startExecuteEdit: ICellEditOperation = { editType: CellEditType.PartialInternalMetadata, handle: this.cellHandle, diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index e93082dac76..8d10507c874 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -35,7 +35,7 @@ import { updateEditorTopPadding } from 'vs/workbench/contrib/notebook/common/not import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { ComplexNotebookProviderInfo, INotebookContentProvider, INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { DiffEditorInputFactoryFunction, EditorInputFactoryFunction, IEditorResolverService, IEditorType, RegisteredEditorInfo, RegisteredEditorPriority, UntitledEditorInputFactoryFunction } from 'vs/workbench/services/editor/common/editorResolverService'; +import { DiffEditorInputFactoryFunction, EditorInputFactoryFunction, EditorInputFactoryObject, IEditorResolverService, IEditorType, RegisteredEditorInfo, RegisteredEditorPriority, UntitledEditorInputFactoryFunction } from 'vs/workbench/services/editor/common/editorResolverService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -195,23 +195,56 @@ export class NotebookProviderInfoStore extends Disposable { const notebookDiffEditorInputFactory: DiffEditorInputFactoryFunction = ({ modified, original, label, description }) => { return { editor: NotebookDiffEditorInput.create(this._instantiationService, modified.resource!, label, description, original.resource!, notebookProviderInfo.id) }; }; + + const notebookFactoryObject: EditorInputFactoryObject = { + createEditorInput: notebookEditorInputFactory, + createDiffEditorInput: notebookDiffEditorInputFactory, + createUntitledEditorInput: notebookUntitledEditorFactory, + }; + const notebookCellFactoryObject: EditorInputFactoryObject = { + createEditorInput: notebookEditorInputFactory, + createDiffEditorInput: notebookDiffEditorInputFactory, + }; + + // TODO @lramos15 find a better way to toggle handling diff editors than needing these listeners for every registration + // This is a lot of event listeners especially if there are many notebooks + disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.textDiffEditorPreview)) { + const canHandleDiff = !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized(); + if (canHandleDiff) { + notebookFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory; + notebookCellFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory; + } else { + notebookFactoryObject.createDiffEditorInput = undefined; + notebookCellFactoryObject.createDiffEditorInput = undefined; + } + } + })); + + disposables.add(this._accessibilityService.onDidChangeScreenReaderOptimized(() => { + const canHandleDiff = !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized(); + if (canHandleDiff) { + notebookFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory; + notebookCellFactoryObject.createDiffEditorInput = notebookDiffEditorInputFactory; + } else { + notebookFactoryObject.createDiffEditorInput = undefined; + notebookCellFactoryObject.createDiffEditorInput = undefined; + } + })); + // Register the notebook editor disposables.add(this._editorResolverService.registerEditor( globPattern, notebookEditorInfo, notebookEditorOptions, - notebookEditorInputFactory, - notebookUntitledEditorFactory, - notebookDiffEditorInputFactory + notebookFactoryObject, )); // Then register the schema handler as exclusive for that notebook disposables.add(this._editorResolverService.registerEditor( `${Schemas.vscodeNotebookCell}:/**/${globPattern}`, { ...notebookEditorInfo, priority: RegisteredEditorPriority.exclusive }, notebookEditorOptions, - notebookEditorInputFactory, - undefined, - notebookDiffEditorInputFactory + notebookCellFactoryObject )); } 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 1d9865937ea..8497edd6c0f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts @@ -4,18 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { disposableTimeout } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { NotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; + +const UPDATE_EXECUTION_ORDER_GRACE_PERIOD = 200; export class CellExecutionPart extends CellPart { private kernelDisposables = this._register(new DisposableStore()); constructor( private readonly _notebookEditor: INotebookEditorDelegate, - private readonly _executionOrderLabel: HTMLElement + private readonly _executionOrderLabel: HTMLElement, + @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService ) { super(); @@ -37,11 +42,22 @@ export class CellExecutionPart extends CellPart { } protected override didRenderCell(element: ICellViewModel): void { - this.updateExecutionOrder(element.internalMetadata); + this.updateExecutionOrder(element.internalMetadata, true); } - private updateExecutionOrder(internalMetadata: NotebookCellInternalMetadata): void { + private updateExecutionOrder(internalMetadata: NotebookCellInternalMetadata, forceClear = false): void { if (this._notebookEditor.activeKernel?.implementsExecutionOrder) { + // If the executionOrder was just cleared, and the cell is executing, wait just a bit before clearing the view to avoid flashing + if (typeof internalMetadata.executionOrder !== 'number' && !forceClear && !!this._notebookExecutionStateService.getCellExecution(this.currentCell!.uri)) { + const renderingCell = this.currentCell; + this.cellDisposables.add(disposableTimeout(() => { + if (this.currentCell === renderingCell) { + this.updateExecutionOrder(this.currentCell!.internalMetadata, true); + } + }, UPDATE_EXECUTION_ORDER_GRACE_PERIOD)); + return; + } + const executionOrderLabel = typeof internalMetadata.executionOrder === 'number' ? `[${internalMetadata.executionOrder}]` : '[ ]'; diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index ace2ec61b1b..ff455936067 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -16,7 +16,6 @@ import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { PrefixSumComputer } from 'vs/editor/common/model/prefixSumComputer'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CursorAtBoundary, ICellViewModel, CellEditState, CellFocusMode, ICellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -30,6 +29,7 @@ import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/vie import { BaseCellRenderTemplate, INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { FastDomNode } from 'vs/base/browser/fastDomNode'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; const enum CellRevealType { Line, @@ -157,9 +157,9 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService + @IInstantiationService instantiationService: IInstantiationService ) { - super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); + super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, instantiationService); NOTEBOOK_CELL_LIST_FOCUSED.bindTo(this.contextKeyService).set(true); this._viewContext = viewContext; this._previousFocusedElements = this.getFocusedElements(); 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 29537af1c02..faa5dabcc3f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -715,7 +715,7 @@ var requirejs = (function() { } if (linkToOpen) { - this.openerService.open(linkToOpen, { fromUserGesture: true, allowCommands: true }); + this.openerService.open(linkToOpen, { fromUserGesture: true, allowCommands: true, fromWorkspace: true }); } break; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 77ed53a9ce3..949c8dd25b5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -313,7 +313,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende templateDisposables.add(scopedInstaService.createInstance(RunToolbar, this.notebookEditor, contextKeyService, container, runButtonContainer)), templateDisposables.add(new CellDecorations(rootContainer, decorationContainer)), templateDisposables.add(scopedInstaService.createInstance(CellComments, this.notebookEditor, cellCommentPartContainer)), - templateDisposables.add(new CellExecutionPart(this.notebookEditor, executionOrderLabel)), + templateDisposables.add(scopedInstaService.createInstance(CellExecutionPart, this.notebookEditor, executionOrderLabel)), templateDisposables.add(scopedInstaService.createInstance(CollapsedCellOutput, this.notebookEditor, cellOutputCollapsedContainer)), templateDisposables.add(new CollapsedCellInput(this.notebookEditor, cellInputCollapsedContainer)), templateDisposables.add(new CellFocusPart(container, focusSinkElement, this.notebookEditor)), diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index 4888d56bd8f..08b8489dcb3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -5,6 +5,7 @@ import type { RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import type { PreloadOptions } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; +import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; interface BaseToWebviewMessage { readonly __vscode_notebook_message: true; @@ -329,6 +330,7 @@ export interface IMarkupCellInitialization { content: string; offset: number; visible: boolean; + metadata: NotebookCellMetadata; } export interface IInitializeMarkupCells { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index f0eb46d08bb..dea460bd27d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -6,6 +6,7 @@ import type { Event } from 'vs/base/common/event'; import type { IDisposable } from 'vs/base/common/lifecycle'; import type * as webviewMessages from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages'; +import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import type * as rendererApi from 'vscode-notebook-renderer'; // !! IMPORTANT !! ---------------------------------------------------------------------------------- @@ -1435,7 +1436,7 @@ async function webviewPreloads(ctx: PreloadContext) { return existing; } - const cell = new MarkupCell(init.cellId, init.mime, init.content, top); + const cell = new MarkupCell(init.cellId, init.mime, init.content, top, init.metadata); cell.element.style.visibility = visible ? 'visible' : 'hidden'; this._markupCells.set(init.cellId, cell); @@ -1634,7 +1635,7 @@ async function webviewPreloads(ctx: PreloadContext) { /// Internal field that holds text content private _content: { readonly value: string; readonly version: number }; - constructor(id: string, mime: string, content: string, top: number) { + constructor(id: string, mime: string, content: string, top: number, metadata: NotebookCellMetadata) { this.id = id; this._content = { value: content, version: 0 }; @@ -1645,7 +1646,7 @@ async function webviewPreloads(ctx: PreloadContext) { this.outputItem = Object.freeze(<rendererApi.OutputItem>{ id, mime, - metadata: undefined, + metadata, text: (): string => { return this._content.value; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 8e13cf654cd..7eca3e5a6cf 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from 'vs/base/common/buffer'; +import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IDiffResult } from 'vs/base/common/diff/diff'; import { Event } from 'vs/base/common/event'; @@ -524,33 +524,45 @@ export namespace CellUri { export const scheme = Schemas.vscodeNotebookCell; - const _regex = /^ch(\d{7,})/; + + const _lengths = ['W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f']; + const _padRegexp = new RegExp(`^[${_lengths.join('')}]+`); + const _radix = 7; export function generate(notebook: URI, handle: number): URI { - return notebook.with({ - scheme, - fragment: `ch${handle.toString().padStart(7, '0')}${notebook.scheme !== Schemas.file ? notebook.scheme : ''}` - }); + + const s = handle.toString(_radix); + const p = s.length < _lengths.length ? _lengths[s.length - 1] : 'z'; + + const fragment = `${p}${s}s${encodeBase64(VSBuffer.fromString(notebook.scheme), true, true)}`; + return notebook.with({ scheme, fragment }); } export function parse(cell: URI): { notebook: URI; handle: number } | undefined { if (cell.scheme !== scheme) { return undefined; } - const match = _regex.exec(cell.fragment); - if (!match) { + + const idx = cell.fragment.indexOf('s'); + if (idx < 0) { + return undefined; + } + + const handle = parseInt(cell.fragment.substring(0, idx).replace(_padRegexp, ''), _radix); + const _scheme = decodeBase64(cell.fragment.substring(idx + 1)).toString(); + + if (isNaN(handle)) { return undefined; } - const handle = Number(match[1]); return { handle, - notebook: cell.with({ - scheme: cell.fragment.substring(match[0].length) || Schemas.file, - fragment: null - }) + notebook: cell.with({ scheme: _scheme, fragment: null }) }; } + + const _regex = /^(\d{8,})(\w[\w\d+.-]*)$/; + export function generateCellOutputUri(notebook: URI, outputId?: string) { return notebook.with({ scheme: Schemas.vscodeNotebookCellOutput, diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts index 7f762509d5b..1fa57c44f31 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCommon.test.ts @@ -295,7 +295,7 @@ suite('CellUri', function () { test('parse, generate (file-scheme)', function () { - const nb = URI.parse('foo:///bar/følder/file.nb'); + const nb = URI.parse('file:///bar/følder/file.nb'); const id = 17; const data = CellUri.generate(nb, id); @@ -316,6 +316,21 @@ suite('CellUri', function () { assert.strictEqual(actual?.handle, id); assert.strictEqual(actual?.notebook.toString(), nb.toString()); }); + + test('stable order', function () { + + const nb = URI.parse('foo:///bar/følder/file.nb'); + const handles = [1, 2, 9, 10, 88, 100, 666666, 7777777]; + + const uris = handles.map(h => CellUri.generate(nb, h)).sort(); + + const strUris = uris.map(String).sort(); + const parsedUris = strUris.map(s => URI.parse(s)); + + const actual = parsedUris.map(u => CellUri.parse(u)?.handle); + + assert.deepStrictEqual(actual, handles); + }); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts index 575b935cfc1..32e71906a47 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts @@ -35,7 +35,9 @@ suite('NotebookProviderInfoStore', function () { }, instantiationService.createInstance(EditorResolverService), new TestConfigurationService(), - new class extends mock<IAccessibilityService>() { }, + new class extends mock<IAccessibilityService>() { + override onDidChangeScreenReaderOptimized: Event<void> = Event.None; + }, instantiationService, new class extends mock<IFileService>() { override hasProvider() { return true; } diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index 1d667408085..033ad539305 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -54,6 +54,8 @@ import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServic import { ResourceMap } from 'vs/base/common/map'; import { TestClipboardService } from 'vs/platform/clipboard/test/common/testClipboardService'; import { IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; export class TestCell extends NotebookCellTextModel { constructor( @@ -173,6 +175,7 @@ export function setupInstantiationService(disposables = new DisposableStore()) { instantiationService.stub(IStorageService, new TestStorageService()); instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(true)); instantiationService.stub(INotebookExecutionStateService, new TestNotebookExecutionStateService()); + instantiationService.stub(IKeybindingService, new MockKeybindingService()); return instantiationService; } diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index f401ea19b4d..55c802041c0 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isObject } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -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_HISTORY, 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 { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_EDITOR_IN_USER_TAB, 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_HISTORY, 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'; @@ -43,7 +43,8 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { KeybindingsEditorInput } from 'vs/workbench/services/preferences/browser/keybindingsEditorInput'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; -import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfileService, CURRENT_PROFILE_CONTEXT } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; @@ -55,6 +56,8 @@ 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_SWITCH_TO_APPLICATION_JSON = 'settings.switchToApplicationJSON'; +const SETTINGS_EDITOR_COMMAND_SWITCH_TO_CURRENT_PROFILE_JSON = 'settings.switchToCurrentProfileJSON'; const SETTINGS_EDITOR_COMMAND_FILTER_ONLINE = 'settings.filterByOnline'; const SETTINGS_EDITOR_COMMAND_FILTER_TELEMETRY = 'settings.filterByTelemetry'; const SETTINGS_EDITOR_COMMAND_FILTER_UNTRUSTED = 'settings.filterUntrusted'; @@ -116,8 +119,9 @@ class SettingsEditor2InputSerializer implements IEditorSerializer { Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer(KeybindingsEditorInput.ID, KeybindingsEditorInputSerializer); Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer(SettingsEditor2Input.ID, SettingsEditor2InputSerializer); -const OPEN_SETTINGS2_ACTION_TITLE = { value: nls.localize('openSettings2', "Open Settings (UI)"), original: 'Open Settings (UI)' }; - +const OPEN_USER_SETTINGS_UI_TITLE = { value: nls.localize('openSettings2', "Open Settings (UI)"), original: 'Open Settings (UI)' }; +const OPEN_USER_SETTINGS_JSON_TITLE = { value: nls.localize('openUserSettingsJson', "Open User Settings (JSON)"), original: 'Open User Settings (JSON)' }; +const OPEN_CURRENT_PROFILE_SETTINGS_JSON_TITLE = { value: nls.localize('openCurrentProfileSettingsJson', "Open Current Profile Settings (JSON)"), original: 'Open Current Profile Settings (JSON)' }; const category = { value: nls.localize('preferences', "Preferences"), original: 'Preferences' }; interface IOpenSettingsActionOptions { @@ -145,6 +149,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @ILabelService private readonly labelService: ILabelService, @IExtensionService private readonly extensionService: IExtensionService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, ) { super(); @@ -157,7 +162,6 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } private registerSettingsActions() { - const that = this; registerAction2(class extends Action2 { constructor() { super({ @@ -203,20 +207,45 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon return accessor.get(IPreferencesService).openSettings({ jsonEditor: false, ...args }); } }); + + const that = this; + const registerOpenSettingsJsonCommandDisposable = this._register(new MutableDisposable()); + const registerOpenSettingsJsonCommand = () => { + registerOpenSettingsJsonCommandDisposable.value = registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.openSettingsJson', + title: that.userDataProfileService.currentProfile.isDefault ? OPEN_USER_SETTINGS_JSON_TITLE : OPEN_CURRENT_PROFILE_SETTINGS_JSON_TITLE, + category, + f1: true, + }); + } + run(accessor: ServicesAccessor, args: IOpenSettingsActionOptions) { + args = sanitizeOpenSettingsArgs(args); + return accessor.get(IPreferencesService).openSettings({ jsonEditor: true, ...args }); + } + }); + }; + registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.action.openSettingsJson', - title: { value: nls.localize('openSettingsJson', "Open Settings (JSON)"), original: 'Open Settings (JSON)' }, + id: 'workbench.action.openApplicationSettingsJson', + title: OPEN_USER_SETTINGS_JSON_TITLE, category, - f1: true, + menu: { + id: MenuId.CommandPalette, + when: ContextKeyExpr.notEquals(CURRENT_PROFILE_CONTEXT.key, that.userDataProfilesService.defaultProfile.id) + } }); } run(accessor: ServicesAccessor, args: IOpenSettingsActionOptions) { args = sanitizeOpenSettingsArgs(args); - return accessor.get(IPreferencesService).openSettings({ jsonEditor: true, ...args }); + return accessor.get(IPreferencesService).openApplicationSettings({ jsonEditor: true, ...args }); } }); + + // Opens the User tab of the Settings editor registerAction2(class extends Action2 { constructor() { super({ @@ -247,15 +276,21 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon const registerOpenUserSettingsEditorFromJsonActionDisposable = this._register(new MutableDisposable()); const registerOpenUserSettingsEditorFromJsonAction = () => { + let when = ContextKeyExpr.and(ResourceContextKey.Resource.isEqualTo(this.userDataProfileService.currentProfile.settingsResource.toString()), ContextKeyExpr.not('isInDiffEditor')); + if (!this.userDataProfileService.currentProfile.isDefault) { + // If the default profile is not active, also show the action when we're in the + // default profile JSON file, which contains the application-scoped settings. + when = ContextKeyExpr.or(when, ContextKeyExpr.and(ResourceContextKey.Resource.isEqualTo(this.userDataProfilesService.defaultProfile.settingsResource.toString()), ContextKeyExpr.not('isInDiffEditor'))); + } registerOpenUserSettingsEditorFromJsonActionDisposable.value = registerAction2(class extends Action2 { constructor() { super({ id: '_workbench.openUserSettingsEditor', - title: OPEN_SETTINGS2_ACTION_TITLE, + title: OPEN_USER_SETTINGS_UI_TITLE, icon: preferencesOpenSettingsIcon, menu: [{ id: MenuId.EditorTitle, - when: ContextKeyExpr.and(ResourceContextKey.Resource.isEqualTo(that.userDataProfileService.currentProfile.settingsResource.toString()), ContextKeyExpr.not('isInDiffEditor')), + when, group: 'navigation', order: 1 }] @@ -267,31 +302,96 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } }); }; - registerOpenUserSettingsEditorFromJsonAction(); - this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => registerOpenUserSettingsEditorFromJsonAction())); - registerAction2(class extends Action2 { - constructor() { - super({ - id: SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON, + const openJsonFromSettingsEditorDisposableStore = this._register(new DisposableStore()); + const registerOpenJsonFromSettingsEditorAction = () => { + openJsonFromSettingsEditorDisposableStore.clear(); + if (!this.userDataProfileService.currentProfile.isDefault) { + // When the default profile is not active, the action for the User tab needs a dropdown + // because User tab settings in that case are actually saved in two separate files. + const submenuId = MenuId.for('PreferencesSubMenu'); + openJsonFromSettingsEditorDisposableStore.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_SWITCH_TO_CURRENT_PROFILE_JSON, + title: OPEN_CURRENT_PROFILE_SETTINGS_JSON_TITLE, + menu: [{ id: submenuId, order: 1 }] + }); + } + run(accessor: ServicesAccessor) { + const editorPane = accessor.get(IEditorService).activeEditorPane; + if (editorPane instanceof SettingsEditor2) { + return editorPane.switchToSettingsFile(); + } + return null; + } + })); + openJsonFromSettingsEditorDisposableStore.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_SWITCH_TO_APPLICATION_JSON, + title: OPEN_USER_SETTINGS_JSON_TITLE, + menu: [{ id: submenuId, order: 2 }] + }); + } + run(accessor: ServicesAccessor) { + const editorPane = accessor.get(IEditorService).activeEditorPane; + if (editorPane instanceof SettingsEditor2) { + return editorPane.switchToApplicationSettingsFile(); + } + return null; + } + })); + openJsonFromSettingsEditorDisposableStore.add(MenuRegistry.appendMenuItem(MenuId.EditorTitle, { title: { value: nls.localize('openSettingsJson', "Open Settings (JSON)"), original: 'Open Settings (JSON)' }, + submenu: submenuId, icon: preferencesOpenSettingsIcon, - menu: [{ - id: MenuId.EditorTitle, - when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()), - group: 'navigation', - order: 1 - }] - }); + when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_EDITOR_IN_USER_TAB, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()), + group: 'navigation', + order: 1 + })); } - run(accessor: ServicesAccessor) { - const editorPane = accessor.get(IEditorService).activeEditorPane; - if (editorPane instanceof SettingsEditor2) { - return editorPane.switchToSettingsFile(); - } - return null; + + let openSettingsJsonWhen = ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()); + if (!this.userDataProfileService.currentProfile.isDefault) { + // If we're not in the default profile, we already created the action for the User tab above, + // so we want to make sure the user is not in the User tab for this more general action. + openSettingsJsonWhen = ContextKeyExpr.and(openSettingsJsonWhen, CONTEXT_SETTINGS_EDITOR_IN_USER_TAB.toNegated()); } - }); + openJsonFromSettingsEditorDisposableStore.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON, + title: { value: nls.localize('openSettingsJson', "Open Settings (JSON)"), original: 'Open Settings (JSON)' }, + icon: preferencesOpenSettingsIcon, + menu: [{ + id: MenuId.EditorTitle, + when: openSettingsJsonWhen, + group: 'navigation', + order: 1 + }] + }); + } + run(accessor: ServicesAccessor) { + const editorPane = accessor.get(IEditorService).activeEditorPane; + if (editorPane instanceof SettingsEditor2) { + return editorPane.switchToSettingsFile(); + } + return null; + } + })); + }; + + registerOpenUserSettingsEditorFromJsonAction(); + registerOpenJsonFromSettingsEditorAction(); + registerOpenSettingsJsonCommand(); + + this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => { + registerOpenUserSettingsEditorFromJsonAction(); + registerOpenJsonFromSettingsEditorAction(); + registerOpenSettingsJsonCommand(); + })); + registerAction2(class extends Action2 { constructor() { super({ @@ -1109,7 +1209,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: commandId, - title: OPEN_SETTINGS2_ACTION_TITLE, + title: OPEN_USER_SETTINGS_UI_TITLE, icon: preferencesOpenSettingsIcon }, when: ContextKeyExpr.and(ResourceContextKey.Resource.isEqualTo(this.preferencesService.workspaceSettingsResource!.toString()), WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.not('isInDiffEditor')), @@ -1134,7 +1234,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: commandId, - title: OPEN_SETTINGS2_ACTION_TITLE, + title: OPEN_USER_SETTINGS_UI_TITLE, icon: preferencesOpenSettingsIcon }, when: ContextKeyExpr.and(ResourceContextKey.Resource.isEqualTo(this.preferencesService.getFolderSettingsResource(folder.uri)!.toString()), ContextKeyExpr.not('isInDiffEditor')), diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index a2aa81b94c1..b64e07f7172 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -40,6 +40,9 @@ import { IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditor import { DefaultSettingsEditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { isEqual } from 'vs/base/common/resources'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export interface IPreferencesRenderer extends IDisposable { render(): void; @@ -481,6 +484,8 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, ) { super(); this._register(this.editor.getModel()!.onDidChangeContent(() => this.delayedRender())); @@ -577,6 +582,22 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc } private handleLocalUserConfiguration(setting: ISetting, configuration: IConfigurationPropertySchema, markerData: IMarkerData[]): void { + if (!this.userDataProfileService.currentProfile.isDefault) { + if (isEqual(this.userDataProfilesService.defaultProfile.settingsResource, this.settingsEditorModel.uri) && configuration.scope !== ConfigurationScope.APPLICATION) { + // If we're in the default profile setting file, and the setting is not + // application-scoped, fade it out. + markerData.push({ + severity: MarkerSeverity.Hint, + tags: [MarkerTag.Unnecessary], + ...setting.range, + message: nls.localize('defaultProfileSettingWhileNonDefaultActive', "This setting cannot be applied while a non-default profile is active. It will be applied when the default profile is active.") + }); + } else if (isEqual(this.userDataProfileService.currentProfile.settingsResource, this.settingsEditorModel.uri) && configuration.scope === ConfigurationScope.APPLICATION) { + // If we're in a profile setting file, and the setting is + // application-scoped, fade it out. + markerData.push(this.generateUnsupportedApplicationSettingMarker(setting)); + } + } if (this.environmentService.remoteAuthority && (configuration.scope === ConfigurationScope.MACHINE || configuration.scope === ConfigurationScope.MACHINE_OVERRIDABLE)) { markerData.push({ severity: MarkerSeverity.Hint, @@ -641,7 +662,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc severity: MarkerSeverity.Hint, tags: [MarkerTag.Unnecessary], ...setting.range, - message: nls.localize('unsupportedApplicationSetting', "This setting can be applied only in application user settings") + message: nls.localize('unsupportedApplicationSetting', "This setting has an application scope and can be set only in the user settings file.") }; } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index cab53e0d6a6..596899b518b 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -37,6 +37,7 @@ import { settingsEditIcon, settingsScopeDropDownIcon } from 'vs/workbench/contri import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { CONTEXT_SETTINGS_EDITOR_IN_USER_TAB } from 'vs/workbench/contrib/preferences/common/preferences'; export class FolderSettingsActionViewItem extends BaseActionViewItem { @@ -205,7 +206,7 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { } } -export type SettingsTarget = ConfigurationTarget.USER_LOCAL | ConfigurationTarget.USER_REMOTE | ConfigurationTarget.WORKSPACE | URI; +export type SettingsTarget = ConfigurationTarget.APPLICATION | ConfigurationTarget.USER_LOCAL | ConfigurationTarget.USER_REMOTE | ConfigurationTarget.WORKSPACE | URI; export interface ISettingsTargetsWidgetOptions { enableRemoteSettings?: boolean; @@ -220,6 +221,7 @@ export class SettingsTargetsWidget extends Widget { private folderSettingsAction!: Action; private folderSettings!: FolderSettingsActionViewItem; private options: ISettingsTargetsWidgetOptions; + private inUserTab: IContextKey<boolean>; private _settingsTarget: SettingsTarget | null = null; @@ -235,12 +237,14 @@ export class SettingsTargetsWidget extends Widget { @ILabelService private readonly labelService: ILabelService, @IPreferencesService private readonly preferencesService: IPreferencesService, @ILanguageService private readonly languageService: ILanguageService, + @IContextKeyService contextKeyService: IContextKeyService, ) { super(); - this.options = options || {}; + this.options = options ?? {}; this.create(parent); this._register(this.contextService.onDidChangeWorkbenchState(() => this.onWorkbenchStateChanged())); this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.update())); + this.inUserTab = CONTEXT_SETTINGS_EDITOR_IN_USER_TAB.bindTo(contextKeyService); } private resetLabels() { @@ -265,12 +269,12 @@ export class SettingsTargetsWidget extends Widget { this.userLocalSettings = new Action('userSettings', '', '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_LOCAL)); this.preferencesService.getEditableSettingsURI(ConfigurationTarget.USER_LOCAL).then(uri => { // Don't wait to create UI on resolving remote - this.userLocalSettings.tooltip = uri?.fsPath || ''; + this.userLocalSettings.tooltip = uri?.fsPath ?? ''; }); this.userRemoteSettings = new Action('userSettingsRemote', '', '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_REMOTE)); this.preferencesService.getEditableSettingsURI(ConfigurationTarget.USER_REMOTE).then(uri => { - this.userRemoteSettings.tooltip = uri?.fsPath || ''; + this.userRemoteSettings.tooltip = uri?.fsPath ?? ''; }); this.workspaceSettings = new Action('workspaceSettings', '', '.settings-tab', false, () => this.updateTarget(ConfigurationTarget.WORKSPACE)); @@ -301,6 +305,7 @@ export class SettingsTargetsWidget extends Widget { } else { this.folderSettings.getAction().checked = false; } + this.inUserTab.set(this.userLocalSettings.checked); } setResultCount(settingsTarget: SettingsTarget, count: number): void { @@ -328,10 +333,11 @@ export class SettingsTargetsWidget extends Widget { if (filter) { const languageToUse = this.languageService.getLanguageName(filter); if (languageToUse) { - this.userLocalSettings.label += ` [${languageToUse}]`; - this.userRemoteSettings.label += ` [${languageToUse}]`; - this.workspaceSettings.label += ` [${languageToUse}]`; - this.folderSettingsAction.label += ` [${languageToUse}]`; + const languageSuffix = ` [${languageToUse}]`; + this.userLocalSettings.label += languageSuffix; + this.userRemoteSettings.label += languageSuffix; + this.workspaceSettings.label += languageSuffix; + this.folderSettingsAction.label += languageSuffix; } } } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 83d1addd26d..20b0969540c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -61,6 +61,8 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { SettingsSearchFilterDropdownMenuActionViewItem } from 'vs/workbench/contrib/preferences/browser/settingsSearchMenu'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ISettingOverrideClickEvent } from 'vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; export const enum SettingsFocusContext { Search, @@ -686,16 +688,31 @@ export class SettingsEditor2 extends EditorPane { } } + switchToApplicationSettingsFile(): Promise<IEditorPane | undefined> { + const query = parseQuery(this.searchWidget.getValue()).query; + return this.openSettingsFile({ query }, true); + } + switchToSettingsFile(): Promise<IEditorPane | undefined> { const query = parseQuery(this.searchWidget.getValue()).query; return this.openSettingsFile({ query }); } - private async openSettingsFile(options?: ISettingsEditorOptions): Promise<IEditorPane | undefined> { + private async openSettingsFile(options?: ISettingsEditorOptions, forceOpenApplicationSettings?: boolean): Promise<IEditorPane | undefined> { const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget; const openOptions: IOpenSettingsOptions = { jsonEditor: true, ...options }; if (currentSettingsTarget === ConfigurationTarget.USER_LOCAL) { + if (options?.revealSetting) { + const configurationProperties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties(); + const configurationScope = configurationProperties[options?.revealSetting.key]?.scope; + if (configurationScope === ConfigurationScope.APPLICATION) { + return this.preferencesService.openApplicationSettings(openOptions); + } + } + if (forceOpenApplicationSettings) { + return this.preferencesService.openApplicationSettings(openOptions); + } return this.preferencesService.openUserSettings(openOptions); } else if (currentSettingsTarget === ConfigurationTarget.USER_REMOTE) { return this.preferencesService.openRemoteSettings(openOptions); @@ -852,7 +869,7 @@ export class SettingsEditor2 extends EditorPane { private createSettingsTree(container: HTMLElement): void { this.settingRenderers = this.instantiationService.createInstance(SettingTreeRenderers); - this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type, e.manualReset))); + this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type, e.manualReset, e.scope))); this._register(this.settingRenderers.onDidOpenSettings(settingKey => { this.openSettingsFile({ revealSetting: { key: settingKey, edit: true } }); })); @@ -939,18 +956,18 @@ export class SettingsEditor2 extends EditorPane { })); } - private onDidChangeSetting(key: string, value: any, type: SettingValueType | SettingValueType[], manualReset: boolean): void { + private onDidChangeSetting(key: string, value: any, type: SettingValueType | SettingValueType[], manualReset: boolean, scope: ConfigurationScope | undefined): void { const parsedQuery = parseQuery(this.searchWidget.getValue()); const languageFilter = parsedQuery.languageFilter; if (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key) { - this.updateChangedSetting(key, value, manualReset, languageFilter); + this.updateChangedSetting(key, value, manualReset, languageFilter, scope); } this.pendingSettingUpdate = { key, value, languageFilter }; if (SettingsEditor2.shouldSettingUpdateFast(type)) { - this.settingFastUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter)); + this.settingFastUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter, scope)); } else { - this.settingSlowUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter)); + this.settingSlowUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter, scope)); } } @@ -1020,12 +1037,12 @@ export class SettingsEditor2 extends EditorPane { return ancestors.reverse(); } - private updateChangedSetting(key: string, value: any, manualReset: boolean, languageFilter: string | undefined): Promise<void> { + private updateChangedSetting(key: string, value: any, manualReset: boolean, languageFilter: string | undefined, scope: ConfigurationScope | undefined): Promise<void> { // ConfigurationService displays the error if this fails. - // Force a render afterwards because onDidConfigurationUpdate doesn't fire if the update doesn't result in an effective setting value change + // Force a render afterwards because onDidConfigurationUpdate doesn't fire if the update doesn't result in an effective setting value change. const settingsTarget = this.settingsTargetsWidget.settingsTarget; const resource = URI.isUri(settingsTarget) ? settingsTarget : undefined; - const configurationTarget = <ConfigurationTarget>(resource ? ConfigurationTarget.WORKSPACE_FOLDER : settingsTarget); + const configurationTarget = <ConfigurationTarget | null>(resource ? ConfigurationTarget.WORKSPACE_FOLDER : settingsTarget) ?? ConfigurationTarget.USER_LOCAL; const overrides: IConfigurationUpdateOverrides = { resource, overrideIdentifiers: languageFilter ? [languageFilter] : undefined }; const configurationTargetIsWorkspace = configurationTarget === ConfigurationTarget.WORKSPACE || configurationTarget === ConfigurationTarget.WORKSPACE_FOLDER; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index ae71ad571f6..f3811ec3287 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -9,10 +9,10 @@ import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLa import { ICustomHover, ITooltipMarkdownString, IUpdatableHoverOptions, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { Emitter } from 'vs/base/common/event'; -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { localize } from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { getDefaultIgnoredSettings, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; @@ -36,15 +36,15 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { private indicatorsContainerElement: HTMLElement; private scopeOverridesElement: HTMLElement; private scopeOverridesLabel: SimpleIconLabel; + private scopeOverridesHover: MutableDisposable<ICustomHover>; private syncIgnoredElement: HTMLElement; private syncIgnoredHover: ICustomHover | undefined; private defaultOverrideIndicatorElement: HTMLElement; private hoverDelegate: IHoverDelegate; - private hover: ICustomHover | undefined; constructor( container: HTMLElement, - @IConfigurationService configurationService: IConfigurationService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IHoverService hoverService: IHoverService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @ILanguageService private readonly languageService: ILanguageService) { @@ -65,6 +65,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { this.scopeOverridesLabel = scopeOverridesIndicator.label; this.syncIgnoredElement = this.createSyncIgnoredElement(); this.defaultOverrideIndicatorElement = this.createDefaultOverrideIndicator(); + this.scopeOverridesHover = new MutableDisposable(); } private createScopeOverridesIndicator(): { element: HTMLElement; label: SimpleIconLabel } { @@ -126,21 +127,33 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { } dispose() { - this.hover?.dispose(); + this.scopeOverridesHover.dispose(); this.syncIgnoredHover?.dispose(); } updateScopeOverrides(element: SettingsTreeSettingElement, elementDisposables: DisposableStore, onDidClickOverrideElement: Emitter<ISettingOverrideClickEvent>) { this.scopeOverridesElement.innerText = ''; this.scopeOverridesElement.style.display = 'none'; - if (element.overriddenScopeList.length || element.overriddenDefaultsLanguageList.length) { + const profileFeatureEnabled = this.configurationService.getValue<boolean>('workbench.experimental.settingsProfiles.enabled'); + if (profileFeatureEnabled && element.matchesScope(ConfigurationTarget.APPLICATION, false)) { + // If the setting is an application-scoped setting, there are no overrides so we can use this + // indicator to display that information instead. + this.scopeOverridesElement.style.display = 'inline'; + this.scopeOverridesElement.classList.add('with-custom-hover'); + + const applicationSettingText = localize('applicationSetting', "Applies to all profiles"); + this.scopeOverridesLabel.text = applicationSettingText; + + const content = localize('applicationSettingDescription', "The setting is not specific to the current profile, and will retain its value when switching profiles."); + this.scopeOverridesHover.value = setupCustomHover(this.hoverDelegate, this.scopeOverridesElement, content); + } else if (element.overriddenScopeList.length || element.overriddenDefaultsLanguageList.length) { if ((MODIFIED_INDICATOR_USE_INLINE_ONLY && element.overriddenScopeList.length) || (element.overriddenScopeList.length === 1 && !element.overriddenDefaultsLanguageList.length)) { // Render inline if we have the flag and there are scope overrides to render, // or if there is only one scope override to render and no language overrides. this.scopeOverridesElement.style.display = 'inline'; this.scopeOverridesElement.classList.remove('with-custom-hover'); - this.hover?.dispose(); + this.scopeOverridesHover.value = undefined; // Just show all the text in the label. const prefaceText = element.isConfigured ? @@ -221,11 +234,10 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { scope: scope as ScopeString, language }); - this.hover!.hide(); + this.scopeOverridesHover.value?.hide(); } }; - this.hover?.dispose(); - this.hover = setupCustomHover(this.hoverDelegate, this.scopeOverridesElement, content, options); + this.scopeOverridesHover.value = setupCustomHover(this.hoverDelegate, this.scopeOverridesElement, content, options); } } this.render(); @@ -281,6 +293,11 @@ function getAccessibleScopeDisplayMidSentenceText(completeScope: string, languag export function getIndicatorsLabelAriaLabel(element: SettingsTreeSettingElement, configurationService: IConfigurationService, languageService: ILanguageService): string { const ariaLabelSections: string[] = []; + const profileFeatureEnabled = configurationService.getValue<boolean>('workbench.experimental.settingsProfiles.enabled'); + if (profileFeatureEnabled && element.matchesScope(ConfigurationTarget.APPLICATION, false)) { + ariaLabelSections.push(localize('applicationSettingDescriptionAccessible', "Setting value retained when switching profiles")); + } + // Add other overrides text const otherOverridesStart = element.isConfigured ? localize('alsoConfiguredIn', "Also modified in") : diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 39df9facda5..90a8b212b6c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -63,6 +63,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { focusedRowBackground, focusedRowBorder, rowHoverBackground, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { getIndicatorsLabelAriaLabel, ISettingOverrideClickEvent, SettingsTreeIndicatorsLabel } from 'vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; const $ = DOM.$; @@ -666,6 +667,7 @@ export interface ISettingChangeEvent { value: any; // undefined => reset/unconfigure type: SettingValueType | SettingValueType[]; manualReset: boolean; + scope: ConfigurationScope | undefined; } export interface ISettingLinkClickEvent { @@ -921,7 +923,13 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.indicatorsLabel.updateScopeOverrides(element, template.elementDisposables, this._onDidClickOverrideElement); - const onChange = (value: any) => this._onDidChangeSetting.fire({ key: element.setting.key, value, type: template.context!.valueType, manualReset: false }); + const onChange = (value: any) => this._onDidChangeSetting.fire({ + key: element.setting.key, + value, + type: template.context!.valueType, + manualReset: false, + scope: element.setting.scope + }); const deprecationText = element.setting.deprecationMessage || ''; if (deprecationText && element.setting.deprecationMessageIsMarkdown) { const disposables = new DisposableStore(); @@ -1505,7 +1513,8 @@ export class SettingExcludeRenderer extends AbstractSettingRenderer implements I key: template.context.setting.key, value: Object.keys(newValue).length === 0 ? undefined : sortKeys(newValue), type: template.context.valueType, - manualReset: false + manualReset: false, + scope: template.context.setting.scope }); } } @@ -1970,7 +1979,13 @@ export class SettingTreeRenderers { new Action('settings.resetSetting', localize('resetSettingLabel', "Reset Setting"), undefined, undefined, async context => { if (context instanceof SettingsTreeSettingElement) { if (!context.isUntrusted) { - this._onDidChangeSetting.fire({ key: context.setting.key, value: undefined, type: context.setting.type as SettingValueType, manualReset: true }); + this._onDidChangeSetting.fire({ + key: context.setting.key, + value: undefined, + type: context.setting.type as SettingValueType, + manualReset: true, + scope: context.setting.scope + }); } } }), diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index 8f7f3f598b8..8238b7b7ee2 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -13,7 +13,7 @@ import { ITOCEntry, knownAcronyms, knownTermMappings, tocData } from 'vs/workben import { ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; import { IExtensionSetting, ISearchResult, ISetting, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, LOCAL_MACHINE_PROFILE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, APPLICATION_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; @@ -167,8 +167,7 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { parent: SettingsTreeGroupElement, inspectResult: IInspectResult, isWorkspaceTrusted: boolean, - private readonly languageService: ILanguageService, - private readonly userDataProfileService: IUserDataProfileService, + private readonly languageService: ILanguageService ) { super(sanitizeId(parent.id + '_' + setting.key)); this.setting = setting; @@ -366,24 +365,25 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { return true; } + if (configTarget === ConfigurationTarget.APPLICATION) { + return APPLICATION_SCOPES.includes(this.setting.scope); + } + if (configTarget === ConfigurationTarget.WORKSPACE_FOLDER) { - return FOLDER_SCOPES.indexOf(this.setting.scope) !== -1; + return FOLDER_SCOPES.includes(this.setting.scope); } if (configTarget === ConfigurationTarget.WORKSPACE) { - return WORKSPACE_SCOPES.indexOf(this.setting.scope) !== -1; + return WORKSPACE_SCOPES.includes(this.setting.scope); } if (configTarget === ConfigurationTarget.USER_REMOTE) { - return REMOTE_MACHINE_SCOPES.indexOf(this.setting.scope) !== -1; + return REMOTE_MACHINE_SCOPES.includes(this.setting.scope); } if (configTarget === ConfigurationTarget.USER_LOCAL) { - if (!this.userDataProfileService.currentProfile.isDefault) { - return LOCAL_MACHINE_PROFILE_SCOPES.indexOf(this.setting.scope) !== -1; - } if (isRemote) { - return LOCAL_MACHINE_SCOPES.indexOf(this.setting.scope) !== -1; + return LOCAL_MACHINE_SCOPES.includes(this.setting.scope); } } @@ -531,9 +531,18 @@ export class SettingsTreeModel { this.updateSettings(arrays.flatten([...this._treeElementsBySettingName.values()]).filter(s => s.isUntrusted)); } + private getTargetToInspect(settingScope: ConfigurationScope | undefined): SettingsTarget { + if (!this._userDataProfileService.currentProfile.isDefault && settingScope === ConfigurationScope.APPLICATION) { + return ConfigurationTarget.APPLICATION; + } else { + return this._viewState.settingsTarget; + } + } + private updateSettings(settings: SettingsTreeSettingElement[]): void { settings.forEach(element => { - const inspectResult = inspectSetting(element.setting.key, this._viewState.settingsTarget, this._viewState.languageFilter, this._configurationService); + const target = this.getTargetToInspect(element.setting.scope); + const inspectResult = inspectSetting(element.setting.key, target, this._viewState.languageFilter, this._configurationService); element.update(inspectResult, this._isWorkspaceTrusted); }); } @@ -569,8 +578,9 @@ export class SettingsTreeModel { } private createSettingsTreeSettingElement(setting: ISetting, parent: SettingsTreeGroupElement): SettingsTreeSettingElement { - const inspectResult = inspectSetting(setting.key, this._viewState.settingsTarget, this._viewState.languageFilter, this._configurationService); - const element = new SettingsTreeSettingElement(setting, parent, inspectResult, this._isWorkspaceTrusted, this._languageService, this._userDataProfileService); + const target = this.getTargetToInspect(setting.scope); + const inspectResult = inspectSetting(setting.key, target, this._viewState.languageFilter, this._configurationService); + const element = new SettingsTreeSettingElement(setting, parent, inspectResult, this._isWorkspaceTrusted, this._languageService); const nameElements = this._treeElementsBySettingName.get(setting.key) || []; nameElements.push(element); @@ -582,7 +592,7 @@ export class SettingsTreeModel { interface IInspectResult { isConfigured: boolean; inspected: IConfigurationValue<unknown>; - targetSelector: 'userLocalValue' | 'userRemoteValue' | 'workspaceValue' | 'workspaceFolderValue'; + targetSelector: 'applicationValue' | 'userLocalValue' | 'userRemoteValue' | 'workspaceValue' | 'workspaceFolderValue'; inspectedLanguageOverrides: Map<string, IConfigurationValue<unknown>>; languageSelector: string | undefined; } @@ -590,17 +600,21 @@ interface IInspectResult { export function inspectSetting(key: string, target: SettingsTarget, languageFilter: string | undefined, configurationService: IWorkbenchConfigurationService): IInspectResult { const inspectOverrides = URI.isUri(target) ? { resource: target } : undefined; const inspected = configurationService.inspect(key, inspectOverrides); - const targetSelector = target === ConfigurationTarget.USER_LOCAL ? 'userLocalValue' : - target === ConfigurationTarget.USER_REMOTE ? 'userRemoteValue' : - target === ConfigurationTarget.WORKSPACE ? 'workspaceValue' : - 'workspaceFolderValue'; - const targetOverrideSelector = target === ConfigurationTarget.USER_LOCAL ? 'userLocal' : - target === ConfigurationTarget.USER_REMOTE ? 'userRemote' : - target === ConfigurationTarget.WORKSPACE ? 'workspace' : - 'workspaceFolder'; + const targetSelector = target === ConfigurationTarget.APPLICATION ? 'applicationValue' : + target === ConfigurationTarget.USER_LOCAL ? 'userLocalValue' : + target === ConfigurationTarget.USER_REMOTE ? 'userRemoteValue' : + target === ConfigurationTarget.WORKSPACE ? 'workspaceValue' : + 'workspaceFolderValue'; + const targetOverrideSelector = target === ConfigurationTarget.APPLICATION ? 'application' : + target === ConfigurationTarget.USER_LOCAL ? 'userLocal' : + target === ConfigurationTarget.USER_REMOTE ? 'userRemote' : + target === ConfigurationTarget.WORKSPACE ? 'workspace' : + 'workspaceFolder'; let isConfigured = typeof inspected[targetSelector] !== 'undefined'; if (!isConfigured) { - if (target === ConfigurationTarget.USER_LOCAL) { + if (target === ConfigurationTarget.APPLICATION) { + isConfigured = !!configurationService.restrictedSettings.application?.includes(key); + } else if (target === ConfigurationTarget.USER_LOCAL) { isConfigured = !!configurationService.restrictedSettings.userLocal?.includes(key); } else if (target === ConfigurationTarget.USER_REMOTE) { isConfigured = !!configurationService.restrictedSettings.userRemote?.includes(key); diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index 39a4cc0d044..fbfc94fa983 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -53,6 +53,7 @@ export const CONTEXT_KEYBINDINGS_EDITOR = new RawContextKey<boolean>('inKeybindi export const CONTEXT_KEYBINDINGS_SEARCH_FOCUS = new RawContextKey<boolean>('inKeybindingsSearch', false); export const CONTEXT_KEYBINDING_FOCUS = new RawContextKey<boolean>('keybindingFocus', false); export const CONTEXT_WHEN_FOCUS = new RawContextKey<boolean>('whenFocus', false); +export const CONTEXT_SETTINGS_EDITOR_IN_USER_TAB = new RawContextKey<boolean>('inSettingsEditorUserTab', false); export const KEYBINDINGS_EDITOR_COMMAND_SEARCH = 'keybindings.editor.searchKeybindings'; export const KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS = 'keybindings.editor.clearSearchResults'; diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index be64900c807..b0f2ac1333d 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -66,35 +66,35 @@ export class PreferencesContribution implements IWorkbenchContribution { label: nls.localize('splitSettingsEditorLabel', "Split Settings Editor"), priority: RegisteredEditorPriority.builtin, }, + {}, { - canHandleDiff: false, - }, - ({ resource, options }): EditorInputWithOptions => { - // Global User Settings File - if (isEqual(resource, this.userDataProfileService.currentProfile.settingsResource)) { - return { editor: this.preferencesService.createSplitJsonEditorInput(ConfigurationTarget.USER_LOCAL, resource), options }; - } + createEditorInput: ({ resource, options }): EditorInputWithOptions => { + // Global User Settings File + if (isEqual(resource, this.userDataProfileService.currentProfile.settingsResource)) { + return { editor: this.preferencesService.createSplitJsonEditorInput(ConfigurationTarget.USER_LOCAL, resource), options }; + } - // Single Folder Workspace Settings File - const state = this.workspaceService.getWorkbenchState(); - if (state === WorkbenchState.FOLDER) { - const folders = this.workspaceService.getWorkspace().folders; - if (isEqual(resource, folders[0].toResource(FOLDER_SETTINGS_PATH))) { - return { editor: this.preferencesService.createSplitJsonEditorInput(ConfigurationTarget.WORKSPACE, resource), options }; + // Single Folder Workspace Settings File + const state = this.workspaceService.getWorkbenchState(); + if (state === WorkbenchState.FOLDER) { + const folders = this.workspaceService.getWorkspace().folders; + if (isEqual(resource, folders[0].toResource(FOLDER_SETTINGS_PATH))) { + return { editor: this.preferencesService.createSplitJsonEditorInput(ConfigurationTarget.WORKSPACE, resource), options }; + } } - } - // Multi Folder Workspace Settings File - else if (state === WorkbenchState.WORKSPACE) { - const folders = this.workspaceService.getWorkspace().folders; - for (const folder of folders) { - if (isEqual(resource, folder.toResource(FOLDER_SETTINGS_PATH))) { - return { editor: this.preferencesService.createSplitJsonEditorInput(ConfigurationTarget.WORKSPACE_FOLDER, resource), options }; + // Multi Folder Workspace Settings File + else if (state === WorkbenchState.WORKSPACE) { + const folders = this.workspaceService.getWorkspace().folders; + for (const folder of folders) { + if (isEqual(resource, folder.toResource(FOLDER_SETTINGS_PATH))) { + return { editor: this.preferencesService.createSplitJsonEditorInput(ConfigurationTarget.WORKSPACE_FOLDER, resource), options }; + } } } - } - return { editor: this.textEditorService.createTextEditor({ resource }), options }; + return { editor: this.textEditorService.createTextEditor({ resource }), options }; + } } ); } diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index e18a8624103..e5ebb7468a5 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -779,10 +779,10 @@ export class RemoteAgentConnectionStatusListener extends Disposable implements I type ReconnectReloadClassification = { owner: 'alexdima'; comment: 'The reload button in the builtin permanent reconnection failure dialog was pressed'; - remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - attempt: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the resolver.' }; + reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the connection.' }; + millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Elapsed time (in ms) since data was last received.' }; + attempt: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The reconnection attempt counter.' }; }; type ReconnectReloadEvent = { remoteName: string | undefined; @@ -825,8 +825,8 @@ export class RemoteAgentConnectionStatusListener extends Disposable implements I type RemoteConnectionLostClassification = { owner: 'alexdima'; comment: 'The remote connection state is now `ConnectionLost`'; - remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the resolver.' }; + reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the connection.' }; }; type RemoteConnectionLostEvent = { remoteName: string | undefined; @@ -861,10 +861,10 @@ export class RemoteAgentConnectionStatusListener extends Disposable implements I type RemoteReconnectionRunningClassification = { owner: 'alexdima'; comment: 'The remote connection state is now `ReconnectionRunning`'; - remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - attempt: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the resolver.' }; + reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the connection.' }; + millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Elapsed time (in ms) since data was last received.' }; + attempt: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The reconnection attempt counter.' }; }; type RemoteReconnectionRunningEvent = { remoteName: string | undefined; @@ -902,11 +902,11 @@ export class RemoteAgentConnectionStatusListener extends Disposable implements I type RemoteReconnectionPermanentFailureClassification = { owner: 'alexdima'; comment: 'The remote connection state is now `ReconnectionPermanentFailure`'; - remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - attempt: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - handled: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the resolver.' }; + reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the connection.' }; + millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Elapsed time (in ms) since data was last received.' }; + attempt: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The reconnection attempt counter.' }; + handled: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error was handled by the resolver.' }; }; type RemoteReconnectionPermanentFailureEvent = { remoteName: string | undefined; @@ -947,10 +947,10 @@ export class RemoteAgentConnectionStatusListener extends Disposable implements I type RemoteConnectionGainClassification = { owner: 'alexdima'; comment: 'The remote connection state is now `ConnectionGain`'; - remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - attempt: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the resolver.' }; + reconnectionToken: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the connection.' }; + millisSinceLastIncomingData: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Elapsed time (in ms) since data was last received.' }; + attempt: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The reconnection attempt counter.' }; }; type RemoteConnectionGainEvent = { remoteName: string | undefined; diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 0b2e94b01bc..1bf27f3153c 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -190,9 +190,9 @@ class InitialRemoteConnectionHealthContribution implements IWorkbenchContributio type RemoteConnectionSuccessClassification = { owner: 'alexdima'; comment: 'The initial connection succeeded'; - web: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + web: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Is web ui.' }; connectionTimeMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time, in ms, until connected'; isMeasurement: true }; - remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the resolver.' }; }; type RemoteConnectionSuccessEvent = { web: boolean; @@ -212,10 +212,10 @@ class InitialRemoteConnectionHealthContribution implements IWorkbenchContributio type RemoteConnectionFailureClassification = { owner: 'alexdima'; comment: 'The initial connection failed'; - web: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + web: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Is web ui.' }; + remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the resolver.' }; connectionTimeMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time, in ms, until connection failure'; isMeasurement: true }; - message: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + message: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Error message' }; }; type RemoteConnectionFailureEvent = { web: boolean; diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 75929726a57..57e7afbf13a 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -19,6 +19,10 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { stripIcons } from 'vs/base/common/iconLabels'; import { Schemas } from 'vs/base/common/network'; import { Iterable } from 'vs/base/common/iterator'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; function getCount(repository: ISCMRepository): number { if (typeof repository.provider.count === 'number') { @@ -272,3 +276,46 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu this.repositoryDisposables.clear(); } } + +class SCMInputTextDocumentLabelFormatter { + + readonly separator = '/'; + + readonly label = (resource: URI) => { + const match = /git\/(?<repositoryId>scm[\d+])\/input/i.exec(resource.path); + + if (match?.groups === undefined) { + return resource.toString(); + } + + const { repositoryId } = match.groups; + const repository = this.scmService.getRepository(repositoryId); + + if (repository === undefined || repository.provider.rootUri === undefined) { + return resource.toString(); + } + + const folder = this.workspaceContextService.getWorkspaceFolder(repository.provider.rootUri); + const repositoryName = folder?.uri.toString() === repository.provider.rootUri.toString() ? folder.name : basename(repository.provider.rootUri); + + return `${repositoryName} (${repository.provider.label})`; + }; + + constructor( + @ISCMService private readonly scmService: ISCMService, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + ) { } +} + +export class SCMInputTextDocumentContribution implements IWorkbenchContribution { + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @ILabelService labelService: ILabelService + ) { + labelService.registerFormatter({ + scheme: Schemas.vscode, + authority: 'scm', + formatting: instantiationService.createInstance(SCMInputTextDocumentLabelFormatter) + }); + } +} diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 956e867a21f..6c0ceafb8ac 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -10,7 +10,7 @@ import { DirtyDiffWorkbenchController } from './dirtydiffDecorator'; import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { SCMActiveResourceContextKeyController, SCMStatusController } from './activity'; +import { SCMActiveResourceContextKeyController, SCMInputTextDocumentContribution, SCMStatusController } from './activity'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -115,6 +115,9 @@ Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(SCMStatusController, LifecyclePhase.Restored); +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(SCMInputTextDocumentContribution, LifecyclePhase.Restored); + Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({ id: 'scm', order: 5, diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 56be4b4065f..e809b5cb465 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1789,7 +1789,8 @@ class SCMInputWidget { const uri = URI.from({ scheme: Schemas.vscode, - path: `scm/${input.repository.provider.contextValue}/${input.repository.provider.id}/input`, + authority: 'scm', + path: `/${input.repository.provider.contextValue}/${input.repository.provider.id}/input`, query }); diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index f59811de9a2..02b440bdf1c 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -243,6 +243,7 @@ } .search-view .monaco-list .monaco-list-row:hover:not(.highlighted) .monaco-action-bar, +.search-view .monaco-list .monaco-list-row.selected .monaco-action-bar, .search-view .monaco-list .monaco-list-row.focused .monaco-action-bar { display: inline-block; } diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index e6be26f64a1..dc0023470ab 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -12,7 +12,7 @@ import * as nls from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { getSelectionKeyboardEvent, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; @@ -23,13 +23,13 @@ import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; -import { FileMatch, FolderMatch, FolderMatchWithResource, Match, RenderableMatch, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, FolderMatch, FolderMatchWithResource, Match, RenderableMatch, searchComparer, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; import { OpenEditorCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; import { SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ISearchConfiguration, VIEW_ID } from 'vs/workbench/services/search/common/search'; +import { ISearchConfiguration, ISearchConfigurationProperties, VIEW_ID } from 'vs/workbench/services/search/common/search'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; export function isSearchViewFocused(viewsService: IViewsService): boolean { @@ -441,6 +441,136 @@ export abstract class AbstractSearchAndReplaceAction extends Action { } } +class ReplaceActionRunner { + constructor( + private viewer: WorkbenchObjectTree<RenderableMatch>, + private viewlet: SearchView | undefined, + private getElementToFocusAfterRemoved: (viewer: WorkbenchObjectTree<RenderableMatch>, elementToBeRemoved: RenderableMatch) => RenderableMatch, + private getPreviousElementAfterRemoved: (viewer: WorkbenchObjectTree<RenderableMatch>, element: RenderableMatch) => RenderableMatch, + // Services + @IReplaceService private readonly replaceService: IReplaceService, + @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + ) { } + + async performReplace(element: FolderMatch | FileMatch | Match): Promise<any> { + // since multiple elements can be selected, we need to check the type of the FolderMatch/FileMatch/Match before we perform the replace. + const elementsToReplace = getElementsToOperateOnInfo(this.viewer, element, this.configurationService.getValue<ISearchConfigurationProperties>('search')); + + await Promise.all(elementsToReplace.map(async (elem) => { + const parent = elem.parent(); + + if ((parent instanceof FolderMatch || parent instanceof FileMatch) && arrayContainsElementOrParent(parent, elementsToReplace)) { + // skip any children who have parents in the array + return; + } + + if (elem instanceof FileMatch) { + elem.parent().replace(elem); + } else if (elem instanceof Match) { + elem.parent().replace(elem); + } else if (elem instanceof FolderMatch) { + await elem.replaceAll(); + } + })); + + const currentBottomFocusElement = elementsToReplace[elementsToReplace.length - 1]; + + if (currentBottomFocusElement instanceof Match) { + const elementToFocus = this.getElementToFocusAfterReplace(currentBottomFocusElement); + + if (elementToFocus) { + this.viewer.setFocus([elementToFocus], getSelectionKeyboardEvent()); + this.viewer.setSelection([elementToFocus], getSelectionKeyboardEvent()); + } + const elementToShowReplacePreview = this.getElementToShowReplacePreview(elementToFocus, currentBottomFocusElement); + + this.viewer.domFocus(); + + const useReplacePreview = this.configurationService.getValue<ISearchConfiguration>().search.useReplacePreview; + if (!useReplacePreview || !elementToShowReplacePreview || this.hasToOpenFile(currentBottomFocusElement)) { + this.viewlet?.open(currentBottomFocusElement, true); + } else { + this.replaceService.openReplacePreview(elementToShowReplacePreview, true); + } + return; + } else { + const nextFocusElement = this.getElementToFocusAfterRemoved(this.viewer, currentBottomFocusElement); + + if (nextFocusElement) { + this.viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent()); + this.viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); + } + + this.viewer.domFocus(); + + if (element instanceof FileMatch) { + this.viewlet?.open(element, true); + } + + return; + } + } + + private getElementToFocusAfterReplace(currElement: Match): RenderableMatch { + const navigator: ITreeNavigator<RenderableMatch | null> = this.viewer.navigate(); + let fileMatched = false; + let elementToFocus: RenderableMatch | null = null; + do { + elementToFocus = navigator.current(); + if (elementToFocus instanceof Match) { + if (elementToFocus.parent().id() === currElement.parent().id()) { + fileMatched = true; + if (currElement.range().getStartPosition().isBeforeOrEqual(elementToFocus.range().getStartPosition())) { + // Closest next match in the same file + break; + } + } else if (fileMatched) { + // First match in the next file (if expanded) + break; + } + } else if (fileMatched) { + if (this.viewer.isCollapsed(elementToFocus)) { + // Next file match (if collapsed) + break; + } + } + } while (!!navigator.next()); + return elementToFocus!; + } + + private getElementToShowReplacePreview(elementToFocus: RenderableMatch, currBottomElem: RenderableMatch): Match | null { + if (this.hasSameParent(elementToFocus, currBottomElem)) { + return <Match>elementToFocus; + } + const previousElement = this.getPreviousElementAfterRemoved(this.viewer, elementToFocus); + if (this.hasSameParent(previousElement, currBottomElem)) { + return <Match>previousElement; + } + return null; + } + + private hasSameParent(element: RenderableMatch, currBottomElem: RenderableMatch): boolean { + if (!(currBottomElem instanceof Match)) { + return false; + } + return element && element instanceof Match && this.uriIdentityService.extUri.isEqual(element.parent().resource, currBottomElem.parent().resource); + } + + private hasToOpenFile(currBottomElem: RenderableMatch): boolean { + if (!(currBottomElem instanceof Match)) { + return false; + } + const activeEditor = this.editorService.activeEditor; + const file = activeEditor?.resource; + if (file) { + return this.uriIdentityService.extUri.isEqual(file, currBottomElem.parent().resource); + } + return false; + } +} + export class RemoveAction extends AbstractSearchAndReplaceAction { static readonly LABEL = nls.localize('RemoveAction.label', "Dismiss"); @@ -448,15 +578,19 @@ export class RemoveAction extends AbstractSearchAndReplaceAction { constructor( private viewer: WorkbenchObjectTree<RenderableMatch>, private element: RenderableMatch, - @IKeybindingService keyBindingService: IKeybindingService + @IKeybindingService keyBindingService: IKeybindingService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(Constants.RemoveActionId, appendKeyBindingLabel(RemoveAction.LABEL, keyBindingService.lookupKeybinding(Constants.RemoveActionId), keyBindingService), ThemeIcon.asClassName(searchRemoveIcon)); } override run(): Promise<any> { - const currentFocusElement = this.viewer.getFocus()[0]; - const nextFocusElement = !currentFocusElement || currentFocusElement instanceof SearchResult || elementIsEqualOrParent(currentFocusElement, this.element) ? - this.getElementToFocusAfterRemoved(this.viewer, this.element) : + const elementsToRemove = getElementsToOperateOnInfo(this.viewer, this.element, this.configurationService.getValue<ISearchConfigurationProperties>('search')); + + const currentBottomFocusElement = elementsToRemove[elementsToRemove.length - 1]; + + const nextFocusElement = !currentBottomFocusElement || currentBottomFocusElement instanceof SearchResult || arrayContainsElementOrParent(currentBottomFocusElement, elementsToRemove) ? + this.getElementToFocusAfterRemoved(this.viewer, <any>currentBottomFocusElement) : null; if (nextFocusElement) { @@ -465,16 +599,18 @@ export class RemoveAction extends AbstractSearchAndReplaceAction { this.viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); } - this.element.parent().remove(<any>this.element); - this.viewer.domFocus(); + elementsToRemove.forEach((currentElement) => + currentElement.parent().remove(<(FolderMatch | FileMatch)[] & Match & FileMatch[]>currentElement) + ); + this.viewer.domFocus(); return Promise.resolve(); } } -function elementIsEqualOrParent(element: RenderableMatch, testParent: RenderableMatch | SearchResult): boolean { +function arrayContainsElementOrParent(element: RenderableMatch, testArray: (RenderableMatch | SearchResult)[]): boolean { do { - if (element === testParent) { + if (testArray.includes(element)) { return true; } } while (!(element.parent() instanceof SearchResult) && (element = <RenderableMatch>element.parent())); @@ -485,49 +621,37 @@ function elementIsEqualOrParent(element: RenderableMatch, testParent: Renderable export class ReplaceAllAction extends AbstractSearchAndReplaceAction { static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All"); - + private replaceRunner: ReplaceActionRunner; constructor( - private viewlet: SearchView, + viewlet: SearchView, private fileMatch: FileMatch, - @IKeybindingService keyBindingService: IKeybindingService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IKeybindingService keyBindingService: IKeybindingService, ) { super(Constants.ReplaceAllInFileActionId, appendKeyBindingLabel(ReplaceAllAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFileActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceAllIcon)); + this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewlet.getControl(), viewlet, this.getElementToFocusAfterRemoved, this.getPreviousElementAfterRemoved); } - override run(): Promise<any> { - const tree = this.viewlet.getControl(); - const nextFocusElement = this.getElementToFocusAfterRemoved(tree, this.fileMatch); - return this.fileMatch.parent().replace(this.fileMatch).then(() => { - if (nextFocusElement) { - tree.setFocus([nextFocusElement], getSelectionKeyboardEvent()); - tree.setSelection([nextFocusElement], getSelectionKeyboardEvent()); - } - - tree.domFocus(); - this.viewlet.open(this.fileMatch, true); - }); + override async run(): Promise<any> { + return this.replaceRunner.performReplace(this.fileMatch); } } export class ReplaceAllInFolderAction extends AbstractSearchAndReplaceAction { static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All"); + private replaceRunner: ReplaceActionRunner; - constructor(private viewer: WorkbenchObjectTree<RenderableMatch>, private folderMatch: FolderMatch, + constructor(viewer: WorkbenchObjectTree<RenderableMatch>, private folderMatch: FolderMatch, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IKeybindingService keyBindingService: IKeybindingService ) { super(Constants.ReplaceAllInFolderActionId, appendKeyBindingLabel(ReplaceAllInFolderAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFolderActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceAllIcon)); + this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewer, undefined, this.getElementToFocusAfterRemoved, this.getPreviousElementAfterRemoved); } override run(): Promise<any> { - const nextFocusElement = this.getElementToFocusAfterRemoved(this.viewer, this.folderMatch); - return this.folderMatch.replaceAll().then(() => { - if (nextFocusElement) { - this.viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent()); - this.viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); - } - this.viewer.domFocus(); - }); + return this.replaceRunner.performReplace(this.folderMatch); } } @@ -536,87 +660,18 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { static readonly LABEL = nls.localize('match.replace.label', "Replace"); static runQ = Promise.resolve(); + private replaceRunner: ReplaceActionRunner; - constructor(private viewer: WorkbenchObjectTree<RenderableMatch>, private element: Match, private viewlet: SearchView, - @IReplaceService private readonly replaceService: IReplaceService, + constructor(viewer: WorkbenchObjectTree<RenderableMatch>, private element: Match, viewlet: SearchView, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IKeybindingService keyBindingService: IKeybindingService, - @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { super(Constants.ReplaceActionId, appendKeyBindingLabel(ReplaceAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceIcon)); + this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewer, viewlet, this.getElementToFocusAfterRemoved, this.getPreviousElementAfterRemoved); } override async run(): Promise<any> { - this.enabled = false; - - await this.element.parent().replace(this.element); - const elementToFocus = this.getElementToFocusAfterReplace(); - if (elementToFocus) { - this.viewer.setFocus([elementToFocus], getSelectionKeyboardEvent()); - this.viewer.setSelection([elementToFocus], getSelectionKeyboardEvent()); - } - - const elementToShowReplacePreview = this.getElementToShowReplacePreview(elementToFocus); - this.viewer.domFocus(); - - const useReplacePreview = this.configurationService.getValue<ISearchConfiguration>().search.useReplacePreview; - if (!useReplacePreview || !elementToShowReplacePreview || this.hasToOpenFile()) { - this.viewlet.open(this.element, true); - } else { - this.replaceService.openReplacePreview(elementToShowReplacePreview, true); - } - } - - private getElementToFocusAfterReplace(): RenderableMatch { - const navigator: ITreeNavigator<RenderableMatch | null> = this.viewer.navigate(); - let fileMatched = false; - let elementToFocus: RenderableMatch | null = null; - do { - elementToFocus = navigator.current(); - if (elementToFocus instanceof Match) { - if (elementToFocus.parent().id() === this.element.parent().id()) { - fileMatched = true; - if (this.element.range().getStartPosition().isBeforeOrEqual(elementToFocus.range().getStartPosition())) { - // Closest next match in the same file - break; - } - } else if (fileMatched) { - // First match in the next file (if expanded) - break; - } - } else if (fileMatched) { - if (this.viewer.isCollapsed(elementToFocus)) { - // Next file match (if collapsed) - break; - } - } - } while (!!navigator.next()); - return elementToFocus!; - } - - private getElementToShowReplacePreview(elementToFocus: RenderableMatch): Match | null { - if (this.hasSameParent(elementToFocus)) { - return <Match>elementToFocus; - } - const previousElement = this.getPreviousElementAfterRemoved(this.viewer, this.element); - if (this.hasSameParent(previousElement)) { - return <Match>previousElement; - } - return null; - } - - private hasSameParent(element: RenderableMatch): boolean { - return element && element instanceof Match && this.uriIdentityService.extUri.isEqual(element.parent().resource, this.element.parent().resource); - } - - private hasToOpenFile(): boolean { - const activeEditor = this.editorService.activeEditor; - const file = activeEditor?.resource; - if (file) { - return this.uriIdentityService.extUri.isEqual(file, this.element.parent().resource); - } - return false; + return this.replaceRunner.performReplace(this.element); } } @@ -767,3 +822,14 @@ export const focusSearchListCommand: ICommandHandler = accessor => { } }); }; + +function getElementsToOperateOnInfo(viewer: WorkbenchObjectTree<RenderableMatch, void>, currElement: RenderableMatch, sortConfig: ISearchConfigurationProperties): RenderableMatch[] { + let elements: RenderableMatch[] = viewer.getSelection().filter((x): x is RenderableMatch => x !== null).sort((a, b) => searchComparer(a, b, sortConfig.sortOrder)); + + // if selection doesn't include multiple elements, just return current focus element. + if (!(elements.length > 1 && elements.includes(currElement))) { + elements = [currElement]; + } + + return elements; +} diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index 976f351c60e..68ae67e464c 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -184,7 +184,7 @@ export class FileMatchRenderer extends Disposable implements ITreeRenderer<FileM renderElement(node: ITreeNode<FileMatch, any>, index: number, templateData: IFileMatchTemplate): void { const fileMatch = node.element; templateData.el.setAttribute('data-resource', fileMatch.resource.toString()); - templateData.label.setFile(fileMatch.resource, { hideIcon: false }); + templateData.label.setFile(fileMatch.resource, { hideIcon: false, fileDecorations: { colors: true, badges: true } }); const count = fileMatch.count(); templateData.badge.setCount(count); templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchMatches', "{0} matches found", count) : nls.localize('searchMatch', "{0} match found", count)); diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 8f87fe9247d..e4bde4f0d05 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -89,6 +89,7 @@ export enum SearchViewPosition { } const SEARCH_CANCELLED_MESSAGE = nls.localize('searchCanceled', "Search was canceled before any results could be found - "); +const DEBOUNCE_DELAY = 75; export class SearchView extends ViewPane { private static readonly ACTIONS_RIGHT_CLASS_NAME = 'actions-right'; @@ -730,7 +731,7 @@ export class SearchView extends ViewPane { } return null; }), - multipleSelectionSupport: false, + multipleSelectionSupport: true, selectionNavigation: true, overrideStyles: { listBackground: this.getBackgroundColor() @@ -743,7 +744,7 @@ export class SearchView extends ViewPane { this._register(this.viewModel.searchResult.onChange(() => updateHasSomeCollapsible())); this._register(this.tree.onDidChangeCollapseState(() => updateHasSomeCollapsible())); - this._register(Event.debounce(this.tree.onDidOpen, (last, event) => event, 75, true)(options => { + this._register(Event.debounce(this.tree.onDidOpen, (last, event) => event, DEBOUNCE_DELAY, true)(options => { if (options.element instanceof Match) { const selectedMatch: Match = options.element; this.currentSelectedFileMatch?.setSelectedMatch(null); @@ -754,6 +755,14 @@ export class SearchView extends ViewPane { } })); + this._register(Event.debounce(this.tree.onDidChangeFocus, (last, event) => event, DEBOUNCE_DELAY, true)(() => { + const selection = this.tree.getSelection(); + const focus = this.tree.getFocus()[0]; + if (selection.length > 1 && focus instanceof Match) { + this.onFocus(focus, true); + } + })); + this._register(Event.any<any>(this.tree.onDidFocus, this.tree.onDidChangeFocus)(() => { if (this.tree.isDOMFocused()) { const focus = this.tree.getFocus()[0]; diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index 4df55f37483..f1d6d8e3f38 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -712,6 +712,41 @@ export function searchMatchComparer(elementA: RenderableMatch, elementB: Rendera return 0; } +export function searchComparer(elementA: RenderableMatch, elementB: RenderableMatch, sortOrder: SearchSortOrder = SearchSortOrder.Default): number { + const elemAParents = createParentList(elementA); + const elemBParents = createParentList(elementB); + + let i = elemAParents.length - 1; + let j = elemBParents.length - 1; + while (i >= 0 && j >= 0) { + if (elemAParents[i].id() !== elemBParents[j].id()) { + return searchMatchComparer(elemAParents[i], elemBParents[j], sortOrder); + } + i--; + j--; + } + const elemAAtEnd = i === 0; + const elemBAtEnd = j === 0; + + if (elemAAtEnd && !elemBAtEnd) { + return 1; + } else if (!elemAAtEnd && elemBAtEnd) { + return -1; + } + return 0; +} + +function createParentList(element: RenderableMatch): RenderableMatch[] { + const parentArray: RenderableMatch[] = []; + let currElement: RenderableMatch | SearchResult = element; + + while (!(currElement instanceof SearchResult)) { + parentArray.push(currElement); + currElement = currElement.parent(); + } + + return parentArray; +} export class SearchResult extends Disposable { private _onChange = this._register(new Emitter<IChangeEvent>()); diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index b0517c6a59f..28cf97e0b77 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; -import { URI as uri } from 'vs/base/common/uri'; +import { URI, URI as uri } from 'vs/base/common/uri'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IModelService } from 'vs/editor/common/services/model'; import { ModelService } from 'vs/editor/common/services/modelService'; @@ -21,7 +21,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; -import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, FolderMatch, Match, searchComparer, searchMatchComparer, SearchModel, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; import { MockLabelService } from 'vs/workbench/services/label/test/common/mockLabelService'; import { IFileMatch, ITextSearchMatch, OneLineRange, QueryType, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -108,12 +108,78 @@ suite('Search - Viewlet', () => { assert(searchMatchComparer(fileMatch3, fileMatch4, SearchSortOrder.Type) < 0); }); - function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ITextSearchMatch[]): FileMatch { + test('Cross-type Comparer', () => { + + const searchResult = aSearchResult(); + const folderMatch1 = aFolderMatch(isWindows ? 'C:\\voo' : '/c/voo', 0, searchResult); + const folderMatch2 = aFolderMatch(isWindows ? 'C:\\with' : '/c/with', 1, searchResult); + + const fileMatch1 = aFileMatch(isWindows ? 'C:\\voo\\foo.a' : '/c/voo/foo.a', folderMatch1); + const fileMatch2 = aFileMatch(isWindows ? 'C:\\with\\path.c' : '/c/with/path.c', folderMatch2); + const fileMatch3 = aFileMatch(isWindows ? 'C:\\with\\path\\bar.b' : '/c/with/path/bar.b', folderMatch2); + + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + + const lineMatch3 = new Match(fileMatch2, ['barfoo'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); + const lineMatch4 = new Match(fileMatch2, ['fooooo'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + + const lineMatch5 = new Match(fileMatch3, ['foobar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + + /*** + * Structure would take the following form: + * + * folderMatch1 (voo) + * > fileMatch1 (/foo.a) + * >> lineMatch1 + * >> lineMatch2 + * folderMatch2 (with) + * > fileMatch2 (/path.c) + * >> lineMatch4 + * >> lineMatch5 + * > fileMatch3 (/path/bar.b) + * >> lineMatch3 + * + */ + + // for these, refer to diagram above + assert(searchComparer(fileMatch1, fileMatch3) < 0); + assert(searchComparer(fileMatch2, fileMatch3) < 0); + assert(searchComparer(folderMatch2, fileMatch2) < 0); + assert(searchComparer(lineMatch4, lineMatch5) < 0); + assert(searchComparer(lineMatch1, lineMatch3) < 0); + assert(searchComparer(lineMatch2, folderMatch2) < 0); + + // travel up hierarchy and order of folders take precedence. "voo < with" in indices + assert(searchComparer(fileMatch1, fileMatch3, SearchSortOrder.FileNames) < 0); + // bar.b < path.c + assert(searchComparer(fileMatch3, fileMatch2, SearchSortOrder.FileNames) < 0); + // lineMatch4's parent is fileMatch2, "bar.b < path.c" + assert(searchComparer(fileMatch3, lineMatch4, SearchSortOrder.FileNames) < 0); + + // bar.b < path.c + assert(searchComparer(fileMatch3, fileMatch2, SearchSortOrder.Type) < 0); + // lineMatch4's parent is fileMatch2, "bar.b < path.c" + assert(searchComparer(fileMatch3, lineMatch4, SearchSortOrder.Type) < 0); + }); + + function aFileMatch(path: string, parentFolder?: FolderMatch, ...lineMatches: ITextSearchMatch[]): FileMatch { const rawMatch: IFileMatch = { resource: uri.file(path), results: lineMatches }; - return instantiation.createInstance(FileMatch, null, null, null, searchResult, rawMatch); + return instantiation.createInstance(FileMatch, null, null, null, parentFolder, rawMatch); + } + + function aFolderMatch(path: string, index: number, parent?: SearchResult): FolderMatch { + const searchModel = instantiation.createInstance(SearchModel); + return instantiation.createInstance(FolderMatch, uri.file(path), path, index, null, parent, searchModel); + } + + function aSearchResult(): SearchResult { + const searchModel = instantiation.createInstance(SearchModel); + searchModel.searchResult.query = { type: 1, folderQueries: [{ folder: URI.parse('file://c:/') }] }; + return searchModel.searchResult; } function stubModelService(instantiationService: TestInstantiationService): IModelService { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index a75b4184bd2..a8a22c38b18 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -88,11 +88,12 @@ class SearchEditorContribution implements IWorkbenchContribution { }, { singlePerResource: true, - canHandleDiff: false, canSupportResource: resource => (extname(resource) === SEARCH_EDITOR_EXT) }, - ({ resource }) => { - return { editor: instantiationService.invokeFunction(getOrMakeSearchEditorInput, { from: 'existingFile', fileUri: resource }) }; + { + createEditorInput: ({ resource }) => { + return { editor: instantiationService.invokeFunction(getOrMakeSearchEditorInput, { from: 'existingFile', fileUri: resource }) }; + } } ); } diff --git a/src/vs/workbench/contrib/snippets/browser/commands/emptyFileSnippets.ts b/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts index 963c92e60cb..2074eef20bd 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/emptyFileSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts @@ -7,6 +7,7 @@ import { groupBy, isFalsyOrEmpty } from 'vs/base/common/arrays'; import { compare } from 'vs/base/common/strings'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IModelService } from 'vs/editor/common/services/model'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { localize } from 'vs/nls'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -16,16 +17,16 @@ import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets import { Snippet } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -export class SelectSnippetForEmptyFile extends SnippetsAction { +export class ApplyFileSnippetAction extends SnippetsAction { - static readonly Id = 'workbench.action.populateFromSnippet'; + static readonly Id = 'workbench.action.populateFileFromSnippet'; constructor() { super({ - id: SelectSnippetForEmptyFile.Id, + id: ApplyFileSnippetAction.Id, title: { - value: localize('label', 'Populate from Snippet'), - original: 'Populate from Snippet' + value: localize('label', 'Populate File from Snippet'), + original: 'Populate File from Snippet' }, f1: true, }); @@ -36,13 +37,14 @@ export class SelectSnippetForEmptyFile extends SnippetsAction { const quickInputService = accessor.get(IQuickInputService); const editorService = accessor.get(IEditorService); const langService = accessor.get(ILanguageService); + const modelService = accessor.get(IModelService); const editor = getCodeEditor(editorService.activeTextEditorControl); if (!editor || !editor.hasModel()) { return; } - const snippets = await snippetService.getSnippets(undefined, { topLevelSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true }); + const snippets = await snippetService.getSnippets(undefined, { fileTemplateSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true }); if (snippets.length === 0) { return; } @@ -60,9 +62,7 @@ export class SelectSnippetForEmptyFile extends SnippetsAction { }]); // set language if possible - if (langService.isRegisteredLanguageId(selection.langId)) { - editor.getModel().setMode(selection.langId); - } + modelService.setMode(editor.getModel(), langService.createById(selection.langId)); } } diff --git a/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts b/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts index aba3f4c5582..5b10727b169 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts @@ -79,7 +79,7 @@ export class InsertSnippetAction extends SnippetEditorAction { }); } - async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, arg: any) { + async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any[]) { const languageService = accessor.get(ILanguageService); const snippetService = accessor.get(ISnippetsService); @@ -94,7 +94,7 @@ export class InsertSnippetAction extends SnippetEditorAction { const snippet = await new Promise<Snippet | undefined>((resolve, reject) => { const { lineNumber, column } = editor.getPosition(); - const { snippet, name, langId } = Args.fromUser(arg); + const { snippet, name, langId } = Args.fromUser(args[0]); if (snippet) { return resolve(new Snippet( diff --git a/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts b/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts index 7a771724f3a..bdd36872158 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts @@ -3,28 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { CodeAction, CodeActionList, CodeActionProvider } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { CodeActionKind } from 'vs/editor/contrib/codeAction/browser/types'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { localize } from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SnippetEditorAction } from 'vs/workbench/contrib/snippets/browser/commands/abstractSnippetsActions'; import { pickSnippet } from 'vs/workbench/contrib/snippets/browser/snippetPicker'; import { Snippet } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { ISnippetsService } from '../snippets'; -async function getSurroundableSnippets(snippetsService: ISnippetsService, model: ITextModel, position: Position, includeDisabledSnippets: boolean): Promise<Snippet[]> { +export async function getSurroundableSnippets(snippetsService: ISnippetsService, model: ITextModel, position: Position, includeDisabledSnippets: boolean): Promise<Snippet[]> { const { lineNumber, column } = position; model.tokenization.tokenizeIfCheap(lineNumber); @@ -83,77 +76,3 @@ export class SurroundWithSnippetEditorAction extends SnippetEditorAction { snippetsService.updateUsageTimestamp(snippet); } } - - -export class SurroundWithSnippetCodeActionProvider implements CodeActionProvider, IWorkbenchContribution { - - private static readonly _MAX_CODE_ACTIONS = 4; - - private static readonly _overflowCommandCodeAction: CodeAction = { - kind: CodeActionKind.Refactor.value, - title: SurroundWithSnippetEditorAction.options.title.value, - command: { - id: SurroundWithSnippetEditorAction.options.id, - title: SurroundWithSnippetEditorAction.options.title.value, - }, - }; - - private readonly _registration: IDisposable; - - constructor( - @ISnippetsService private readonly _snippetService: ISnippetsService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, - ) { - this._registration = languageFeaturesService.codeActionProvider.register('*', this); - } - - dispose(): void { - this._registration.dispose(); - } - - async provideCodeActions(model: ITextModel, range: Range | Selection): Promise<CodeActionList | undefined> { - - if (range.isEmpty()) { - return undefined; - } - - const position = Selection.isISelection(range) ? range.getPosition() : range.getStartPosition(); - const snippets = await getSurroundableSnippets(this._snippetService, model, position, false); - if (!snippets.length) { - return undefined; - } - - const actions: CodeAction[] = []; - const hasMore = snippets.length > SurroundWithSnippetCodeActionProvider._MAX_CODE_ACTIONS; - const len = Math.min(snippets.length, SurroundWithSnippetCodeActionProvider._MAX_CODE_ACTIONS); - - for (let i = 0; i < len; i++) { - actions.push(this._makeCodeActionForSnippet(snippets[i], model, range)); - } - if (hasMore) { - actions.push(SurroundWithSnippetCodeActionProvider._overflowCommandCodeAction); - } - return { - actions, - dispose() { } - }; - } - - private _makeCodeActionForSnippet(snippet: Snippet, model: ITextModel, range: IRange): CodeAction { - return { - title: localize('codeAction', "Surround With: {0}", snippet.name), - kind: CodeActionKind.Refactor.value, - edit: { - edits: [{ - versionId: model.getVersionId(), - resource: model.uri, - textEdit: { - range, - text: snippet.body, - insertAsSnippet: true, - } - }] - } - }; - } -} diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts new file mode 100644 index 00000000000..cef9225c933 --- /dev/null +++ b/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IRange, Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { CodeAction, CodeActionList, CodeActionProvider, WorkspaceEdit } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { CodeActionKind } from 'vs/editor/contrib/codeAction/browser/types'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { ApplyFileSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets'; +import { getSurroundableSnippets, SurroundWithSnippetEditorAction } from 'vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet'; +import { Snippet } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; +import { ISnippetsService } from './snippets'; + +class SurroundWithSnippetCodeActionProvider implements CodeActionProvider { + + private static readonly _MAX_CODE_ACTIONS = 4; + + private static readonly _overflowCommandCodeAction: CodeAction = { + kind: CodeActionKind.Refactor.value, + title: SurroundWithSnippetEditorAction.options.title.value, + command: { + id: SurroundWithSnippetEditorAction.options.id, + title: SurroundWithSnippetEditorAction.options.title.value, + }, + }; + + constructor(@ISnippetsService private readonly _snippetService: ISnippetsService) { } + + async provideCodeActions(model: ITextModel, range: Range | Selection): Promise<CodeActionList | undefined> { + + if (range.isEmpty()) { + return undefined; + } + + const position = Selection.isISelection(range) ? range.getPosition() : range.getStartPosition(); + const snippets = await getSurroundableSnippets(this._snippetService, model, position, false); + if (!snippets.length) { + return undefined; + } + + const actions: CodeAction[] = []; + for (const snippet of snippets) { + if (actions.length >= SurroundWithSnippetCodeActionProvider._MAX_CODE_ACTIONS) { + actions.push(SurroundWithSnippetCodeActionProvider._overflowCommandCodeAction); + break; + } + actions.push({ + title: localize('codeAction', "Surround With: {0}", snippet.name), + kind: CodeActionKind.Refactor.value, + edit: asWorkspaceEdit(model, range, snippet) + }); + } + + return { + actions, + dispose() { } + }; + } +} + +class FileTemplateCodeActionProvider implements CodeActionProvider { + + private static readonly _MAX_CODE_ACTIONS = 4; + + private static readonly _overflowCommandCodeAction: CodeAction = { + title: localize('overflow.start.title', 'Start with Snippet'), + kind: CodeActionKind.Refactor.value, + command: { + id: ApplyFileSnippetAction.Id, + title: '' + } + }; + + readonly providedCodeActionKinds?: readonly string[] = [CodeActionKind.Refactor.value]; + + constructor(@ISnippetsService private readonly _snippetService: ISnippetsService) { } + + async provideCodeActions(model: ITextModel) { + if (model.getValueLength() !== 0) { + return undefined; + } + + const snippets = await this._snippetService.getSnippets(model.getLanguageId(), { fileTemplateSnippets: true, includeNoPrefixSnippets: true }); + const actions: CodeAction[] = []; + for (const snippet of snippets) { + if (actions.length >= FileTemplateCodeActionProvider._MAX_CODE_ACTIONS) { + actions.push(FileTemplateCodeActionProvider._overflowCommandCodeAction); + break; + } + actions.push({ + title: localize('title', 'Start with: {0}', snippet.name), + kind: CodeActionKind.Refactor.value, + edit: asWorkspaceEdit(model, model.getFullModelRange(), snippet) + }); + } + return { + actions, + dispose() { } + }; + } +} + +function asWorkspaceEdit(model: ITextModel, range: IRange, snippet: Snippet): WorkspaceEdit { + return { + edits: [{ + versionId: model.getVersionId(), + resource: model.uri, + textEdit: { + range, + text: snippet.body, + insertAsSnippet: true, + } + }] + }; +} + +export class SnippetCodeActions implements IWorkbenchContribution { + + private readonly _store = new DisposableStore(); + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @IConfigurationService configService: IConfigurationService, + ) { + + const setting = 'editor.snippets.codeActions.enabled'; + const sessionStore = new DisposableStore(); + const update = () => { + sessionStore.clear(); + if (configService.getValue(setting)) { + sessionStore.add(languageFeaturesService.codeActionProvider.register('*', instantiationService.createInstance(SurroundWithSnippetCodeActionProvider))); + sessionStore.add(languageFeaturesService.codeActionProvider.register('*', instantiationService.createInstance(FileTemplateCodeActionProvider))); + } + }; + + update(); + this._store.add(configService.onDidChangeConfiguration(e => e.affectsConfiguration(setting) && update())); + this._store.add(sessionStore); + } + + dispose(): void { + this._store.dispose(); + } +} diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts index 39f0c8233c5..4da8e2b191d 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts @@ -12,14 +12,17 @@ import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonCo import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { ConfigureSnippets } from 'vs/workbench/contrib/snippets/browser/commands/configureSnippets'; -import { SelectSnippetForEmptyFile } from 'vs/workbench/contrib/snippets/browser/commands/emptyFileSnippets'; +import { ApplyFileSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets'; import { InsertSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/insertSnippet'; -import { SurroundWithSnippetCodeActionProvider, SurroundWithSnippetEditorAction } from 'vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet'; +import { SurroundWithSnippetEditorAction } from 'vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet'; +import { SnippetCodeActions } from 'vs/workbench/contrib/snippets/browser/snippetCodeActionProvider'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets'; import { SnippetsService } from 'vs/workbench/contrib/snippets/browser/snippetsService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import 'vs/workbench/contrib/snippets/browser/tabCompletion'; +import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; // service registerSingleton(ISnippetsService, SnippetsService, true); @@ -29,10 +32,26 @@ registerAction2(InsertSnippetAction); CommandsRegistry.registerCommandAlias('editor.action.showSnippets', 'editor.action.insertSnippet'); registerAction2(SurroundWithSnippetEditorAction); registerAction2(ConfigureSnippets); -registerAction2(SelectSnippetForEmptyFile); +registerAction2(ApplyFileSnippetAction); // workbench contribs -Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SurroundWithSnippetCodeActionProvider, LifecyclePhase.Restored); +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(SnippetCodeActions, LifecyclePhase.Restored); + +// config +Registry + .as<IConfigurationRegistry>(Extensions.Configuration) + .registerConfiguration({ + ...editorConfigurationBaseNode, + 'properties': { + 'editor.snippets.codeActions.enabled': { + 'description': nls.localize('editor.snippets.codeActions.enabled', 'Controls if surround-with-snippets or file template snippets show as code actions.'), + 'type': 'boolean', + 'default': true + } + } + }); + // schema const languageScopeSchemaId = 'vscode://schemas/snippets'; @@ -42,8 +61,8 @@ const snippetSchemaProperties: IJSONSchemaMap = { description: nls.localize('snippetSchema.json.prefix', 'The prefix to use when selecting the snippet in intellisense'), type: ['string', 'array'] }, - isTopLevel: { - description: nls.localize('snippetSchema.json.isTopLevel', 'The snippet is only applicable to empty files.'), + isFileTemplate: { + description: nls.localize('snippetSchema.json.isFileTemplate', 'The snippet is meant to populate or replace a whole file'), type: 'boolean' }, body: { diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.ts b/src/vs/workbench/contrib/snippets/browser/snippets.ts index fa485ab0f2f..a11c4ccf6a8 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.ts @@ -12,7 +12,7 @@ export interface ISnippetGetOptions { includeDisabledSnippets?: boolean; includeNoPrefixSnippets?: boolean; noRecencySort?: boolean; - topLevelSnippets?: boolean; + fileTemplateSnippets?: boolean; } export interface ISnippetsService { diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index b6ce272ef28..070fd986cdf 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -105,7 +105,7 @@ export class Snippet { readonly prefixLow: string; constructor( - readonly isTopLevel: boolean, + readonly isFileTemplate: boolean, readonly scopes: string[], readonly name: string, readonly prefix: string, @@ -143,7 +143,7 @@ export class Snippet { interface JsonSerializedSnippet { - isTopLevel?: boolean; + isFileTemplate?: boolean; body: string | string[]; scope?: string; prefix: string | string[] | undefined; @@ -261,7 +261,7 @@ export class SnippetFile { private _parseSnippet(name: string, snippet: JsonSerializedSnippet, bucket: Snippet[]): void { - let { isTopLevel, prefix, body, description } = snippet; + let { isFileTemplate, prefix, body, description } = snippet; if (!prefix) { prefix = ''; @@ -306,7 +306,7 @@ export class SnippetFile { for (const _prefix of Array.isArray(prefix) ? prefix : Iterable.single(prefix)) { bucket.push(new Snippet( - Boolean(isTopLevel), + Boolean(isFileTemplate), scopes, name, _prefix, diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 3c5ed85f665..069f3bf44f3 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -318,7 +318,7 @@ export class SnippetsService implements ISnippetsService { // enabled or disabled wanted continue; } - if (typeof opts?.topLevelSnippets === 'boolean' && opts.topLevelSnippets !== snippet.isTopLevel) { + if (typeof opts?.fileTemplateSnippets === 'boolean' && opts.fileTemplateSnippets !== snippet.isFileTemplate) { // isTopLevel requested but mismatching continue; } diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts index d50317952c0..30d07dcc5b3 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts @@ -820,7 +820,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { return this.parentURI(filesToOpenOrCreate[0].fileUri); } else if (filesToDiff && filesToDiff.length) { return this.parentURI(filesToDiff[0].fileUri); - } else if (filesToMerge && filesToMerge.length) { + } else if (filesToMerge && filesToMerge.length === 4) { return this.parentURI(filesToMerge[3].fileUri) /* [3] is the resulting merge file */; } return undefined; diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 4bfc497f05a..dbe5a62e3be 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -293,7 +293,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer })); this._taskRunningState = TASK_RUNNING_STATE.bindTo(_contextKeyService); this._onDidStateChange = this._register(new Emitter()); - this._registerCommands(); + this._registerCommands().then(() => { + TaskCommandsRegistered.bindTo(this._contextKeyService).set(true); + }); this._configurationResolverService.contributeVariable('defaultBuildTask', async (): Promise<string | undefined> => { let tasks = await this._getTasksForGroup(TaskGroup.Build); if (tasks.length > 0) { @@ -394,13 +396,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer properties: { type: { type: 'string', - description: nls.localize('runTask.type', "The contributed task type"), - enum: Array.from(this._providerTypes.values()).map(provider => provider) + description: nls.localize('runTask.type', "The contributed task type") }, taskName: { type: 'string', - description: nls.localize('runTask.taskName', "The task's label or a term to filter by"), - enum: await this.tasks().then((tasks) => tasks.map(t => t._label)) + description: nls.localize('runTask.taskName', "The task's label or a term to filter by") } } } @@ -491,7 +491,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this._openTaskFile(resource, TaskSourceKind.WorkspaceFile); } }); - TaskCommandsRegistered.bindTo(this._contextKeyService).set(true); } private get workspaceFolders(): IWorkspaceFolder[] { @@ -2173,7 +2172,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } private get _jsonTasksSupported(): boolean { - return !!ShellExecutionSupportedContext.getValue(this._contextKeyService) && !!ProcessExecutionSupportedContext.getValue(this._contextKeyService); + return ShellExecutionSupportedContext.getValue(this._contextKeyService) === true && ProcessExecutionSupportedContext.getValue(this._contextKeyService) === true; } private _computeWorkspaceFolderTasks(workspaceFolder: IWorkspaceFolder, runSource: TaskRunSource = TaskRunSource.User): Promise<IWorkspaceFolderTaskResult> { diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 3b7aa6162af..658d86f53b2 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -40,7 +40,7 @@ import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDe import { TerminalMenuBarGroup } from 'vs/workbench/contrib/terminal/browser/terminalMenus'; import { isString } from 'vs/base/common/types'; -const SHOW_TASKS_COMMANDS_CONTEXT = ContextKeyExpr.or(ShellExecutionSupportedContext, ProcessExecutionSupportedContext); +const SHOW_TASKS_COMMANDS_CONTEXT = ContextKeyExpr.and(ShellExecutionSupportedContext, ProcessExecutionSupportedContext); const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(RunAutomaticTasks, LifecyclePhase.Eventually); @@ -112,7 +112,7 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench } if (promise && (event.kind === TaskEventKind.Active) && (this._activeTasksCount === 1)) { - this._progressService.withProgress({ location: ProgressLocation.Window, command: 'workbench.action.tasks.showTasks' }, progress => { + this._progressService.withProgress({ location: ProgressLocation.Window, command: 'workbench.action.tasks.showTasks', type: 'loading' }, progress => { progress.report({ message: nls.localize('building', 'Building...') }); return promise!; }).then(() => { diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 674741ed4d0..40346309f5e 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -36,11 +36,11 @@ type TelemetryData = { }; type FileTelemetryDataFragment = { - mimeType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - path: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - reason?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - allowlistedjson?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + mimeType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language type of the file (for example XML).' }; + ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The file extension of the file (for example xml).' }; + path: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The path of the file as a hash.' }; + reason?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The reason why a file is read or written. Allows to e.g. distinguish auto save from normal save.' }; + allowlistedjson?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the file but only if it matches some well known file names such as package.json or tsconfig.json.' }; }; export class TelemetryContribution extends Disposable implements IWorkbenchContribution { @@ -67,27 +67,29 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr const activeViewlet = paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar); type WindowSizeFragment = { - innerHeight: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - innerWidth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - outerHeight: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - outerWidth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + innerHeight: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The height of the current window.' }; + innerWidth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The width of the current window.' }; + outerHeight: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The height of the current window with all decoration removed.' }; + outerWidth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The width of the current window with all decoration removed.' }; + comment: 'The size of the window.'; }; type WorkspaceLoadClassification = { owner: 'bpasero'; - userAgent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - emptyWorkbench: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + userAgent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The user agent as reported by `navigator.userAgent` by Electron or the web browser.' }; + emptyWorkbench: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether a folder or workspace is opened or not.' }; windowSize: WindowSizeFragment; - 'workbench.filesToOpenOrCreate': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - 'workbench.filesToDiff': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - 'workbench.filesToMerge': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + 'workbench.filesToOpenOrCreate': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of files that should open or be created.' }; + 'workbench.filesToDiff': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of files that should be compared.' }; + 'workbench.filesToMerge': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of files that should be merged.' }; customKeybindingsCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - theme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - language: { classification: 'SystemMetaData'; purpose: 'BusinessInsight' }; - pinnedViewlets: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - restoredViewlet?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - restoredEditors: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - startupKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + theme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The current theme of the window.' }; + language: { classification: 'SystemMetaData'; purpose: 'BusinessInsight'; comment: 'The display language of the window.' }; + pinnedViewlets: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifiers of views that are pinned.' }; + restoredViewlet?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the view that is restored.' }; + restoredEditors: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of editors that restored.' }; + startupKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'How the window was opened, e.g via reload or not.' }; + comment: 'Metadata around the workspace that is being loaded into a window.'; }; type WorkspaceLoadEvent = { @@ -141,13 +143,15 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr if (settingsType) { type SettingsReadClassification = { owner: 'bpasero'; - settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the settings file that was read.' }; + comment: 'Track when a settings file was read, for example from an editor.'; }; this.telemetryService.publicLog2<{ settingsType: string }, SettingsReadClassification>('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data } else { type FileGetClassification = { owner: 'bpasero'; + comment: 'Track when a file was read, for example from an editor.'; } & FileTelemetryDataFragment; this.telemetryService.publicLog2<TelemetryData, FileGetClassification>('fileGet', this.getTelemetryData(e.model.resource, e.reason)); @@ -159,12 +163,14 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr if (settingsType) { type SettingsWrittenClassification = { owner: 'bpasero'; - settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the settings file that was written to.' }; + comment: 'Track when a settings file was written to, for example from an editor.'; }; this.telemetryService.publicLog2<{ settingsType: string }, SettingsWrittenClassification>('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data } else { type FilePutClassfication = { owner: 'bpasero'; + comment: 'Track when a file was written to, for example from an editor.'; } & FileTelemetryDataFragment; this.telemetryService.publicLog2<TelemetryData, FilePutClassfication>('filePUT', this.getTelemetryData(e.model.resource, e.reason)); } diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts index 6fc5f4eb043..b087c9d94df 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts @@ -23,6 +23,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { ISearchService } from 'vs/workbench/services/search/common/search'; +import { basename } from 'vs/base/common/path'; export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener { constructor( @@ -35,7 +36,7 @@ export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener { if (!link.uri) { throw new Error('Tried to open file link without a resolved URI'); } - const lineColumnInfo: ILineColumnInfo = this.extractLineColumnInfo(link.text); + const lineColumnInfo: ILineColumnInfo = this.extractLineColumnInfo(link.text, link.uri); const selection: ITextEditorSelection = { startLineNumber: lineColumnInfo.lineNumber, startColumn: lineColumnInfo.columnNumber @@ -51,12 +52,22 @@ export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener { * * @param link Url link which may contain line and column number. */ - extractLineColumnInfo(link: string): ILineColumnInfo { + extractLineColumnInfo(link: string, uri: URI): ILineColumnInfo { const lineColumnInfo: ILineColumnInfo = { lineNumber: 1, columnNumber: 1 }; + // Calculate the file name end using the URI if possible, this will help with sanitizing the + // link for the match regex. The actual path isn't important in extracting the line and + // column from the regex so modifying the link text before the file name is safe. + const fileName = basename(uri.path); + const index = link.indexOf(fileName); + const fileNameEndIndex: number = index !== -1 ? index + fileName.length : link.length; + + // Sanitize the link text such that the folders and file name do not contain whitespace. + link = link.slice(0, fileNameEndIndex).replace(/\s/g, '_') + link.slice(fileNameEndIndex); + // The local link regex only works for non file:// links, check these for a simple // `:line:col` suffix if (link.startsWith('file://')) { @@ -248,7 +259,7 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { const { uri, isDirectory } = result; const linkToOpen = { // Use the absolute URI's path here so the optional line/col get detected - text: result.uri.fsPath + (text.match(/:\d+(:\d+)?$/)?.[0] || ''), + text: result.uri.path + (text.match(/:\d+(:\d+)?$/)?.[0] || ''), uri, bufferRange: link.bufferRange, type: link.type diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index a2f4afa1400..a7b1e8acf36 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -33,12 +33,6 @@ if [ "$VSCODE_INJECTION" == "1" ]; then builtin unset VSCODE_INJECTION fi -# Disable shell integration if PROMPT_COMMAND is 2+ function calls since that is not handled. -if [[ "$PROMPT_COMMAND" =~ .*(' '.*\;)|(\;.*' ').* ]]; then - builtin unset VSCODE_SHELL_INTEGRATION - builtin return -fi - if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then builtin return fi @@ -155,25 +149,16 @@ __vsc_update_prompt __vsc_prompt_cmd_original() { __vsc_status="$?" - if [[ ${IFS+set} ]]; then - __vsc_original_ifs="$IFS" - fi - if [[ "$__vsc_original_prompt_command" =~ .+\;.+ ]]; then - IFS=';' + # Evaluate the original PROMPT_COMMAND similarly to how bash would normally + # See https://unix.stackexchange.com/a/672843 for technique + if [[ ${#__vsc_original_prompt_command[@]} -gt 1 ]]; then + for cmd in "${__vsc_original_prompt_command[@]}"; do + __vsc_status="$?" + eval "${cmd:-}" + done else - IFS=' ' + eval "${__vsc_original_prompt_command:-}" fi - builtin read -ra ADDR <<<"$__vsc_original_prompt_command" - if [[ ${__vsc_original_ifs+set} ]]; then - IFS="$__vsc_original_ifs" - unset __vsc_original_ifs - else - unset IFS - fi - for ((i = 0; i < ${#ADDR[@]}; i++)); do - (exit ${__vsc_status}) - builtin eval ${ADDR[i]} - done __vsc_precmd } @@ -182,13 +167,9 @@ __vsc_prompt_cmd() { __vsc_precmd } -if [[ "$PROMPT_COMMAND" =~ (.+\;.+) ]]; then - # item1;item2... - __vsc_original_prompt_command="$PROMPT_COMMAND" -else - # (item1, item2...) - __vsc_original_prompt_command=${PROMPT_COMMAND[@]} -fi +# PROMPT_COMMAND arrays and strings seem to be handled the same (handling only the first entry of +# the array?) +__vsc_original_prompt_command=$PROMPT_COMMAND if [[ -z "${bash_preexec_imported:-}" ]]; then if [[ -n "$__vsc_original_prompt_command" && "$__vsc_original_prompt_command" != "__vsc_prompt_cmd" ]]; then diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 16275cb5114..b186c032261 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -449,6 +449,11 @@ .terminal-scroll-highlight { left: 0; right: 0; + border-left: 5px solid #ffffff; + border-left-width: 5px !important; +} + +.terminal-scroll-highlight-outline { border: 1px solid #ffffff; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index bbfb4d97207..09736ecd0b0 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -831,8 +831,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return; } let placeholder: string; - type Item = IQuickPickItem & { command?: ITerminalCommand }; - let items: (Item | IQuickPickItem | IQuickPickSeparator)[] = []; + type Item = IQuickPickItem & { command?: ITerminalCommand; rawLabel: string }; + let items: (Item | IQuickPickItem & { rawLabel: string } | IQuickPickSeparator)[] = []; const commandMap: Set<string> = new Set(); const removeFromCommandHistoryButton: IQuickInputButton = { @@ -849,9 +849,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (executingCommand) { commandMap.add(executingCommand); } + function formatLabel(label: string) { + return label.replace(/\r?\n/g, '\u23CE'); + } if (commands && commands.length > 0) { for (const entry of commands) { - // trim off any whitespace and/or line endings + // Trim off any whitespace and/or line endings, replace new lines with the + // Downwards Arrow with Corner Leftwards symbol const label = entry.command.trim(); if (label.length === 0 || commandMap.has(label)) { continue; @@ -881,7 +885,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { continue; } items.push({ - label, + label: formatLabel(label), + rawLabel: label, description, id: entry.timestamp.toString(), command: entry, @@ -893,7 +898,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } if (executingCommand) { items.unshift({ - label: executingCommand, + label: formatLabel(executingCommand), + rawLabel: executingCommand, description: cmdDetection.cwd }); } @@ -903,12 +909,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Gather previous session history const history = this._instantiationService.invokeFunction(getCommandHistory); - const previousSessionItems: IQuickPickItem[] = []; + const previousSessionItems: (IQuickPickItem & { rawLabel: string })[] = []; for (const [label, info] of history.entries) { // Only add previous session item if it's not in this session if (!commandMap.has(label) && info.shellType === this.shellType) { previousSessionItems.unshift({ - label, + label: formatLabel(label), + rawLabel: label, buttons: [removeFromCommandHistoryButton] }); commandMap.add(label); @@ -924,10 +931,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Gather shell file history const shellFileHistory = await this._instantiationService.invokeFunction(getShellFileHistory, this._shellType); - const dedupedShellFileItems: IQuickPickItem[] = []; + const dedupedShellFileItems: (IQuickPickItem & { rawLabel: string })[] = []; for (const label of shellFileHistory) { if (!commandMap.has(label)) { - dedupedShellFileItems.unshift({ label }); + dedupedShellFileItems.unshift({ + label: formatLabel(label), + rawLabel: label + }); } } if (dedupedShellFileItems.length > 0) { @@ -943,7 +953,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const cwds = this.capabilities.get(TerminalCapability.CwdDetection)?.cwds || []; if (cwds && cwds.length > 0) { for (const label of cwds) { - items.push({ label }); + items.push({ label, rawLabel: label }); } items = items.reverse(); items.unshift({ type: 'separator', label: terminalStrings.currentSessionCategory }); @@ -951,12 +961,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Gather previous session history const history = this._instantiationService.invokeFunction(getDirectoryHistory); - const previousSessionItems: IQuickPickItem[] = []; + const previousSessionItems: (IQuickPickItem & { rawLabel: string })[] = []; // Only add previous session item if it's not in this session and it matches the remote authority for (const [label, info] of history.entries) { if ((info === null || info.remoteAuthority === this.remoteAuthority) && !cwds.includes(label)) { previousSessionItems.unshift({ label, + rawLabel: label, buttons: [removeFromCommandHistoryButton] }); } @@ -972,7 +983,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return; } const outputProvider = this._instantiationService.createInstance(TerminalOutputProvider); - const quickPick = this._quickInputService.createQuickPick(); + const quickPick = this._quickInputService.createQuickPick<IQuickPickItem & { rawLabel: string }>(); const originalItems = items; quickPick.items = [...originalItems]; quickPick.sortByLabel = false; @@ -1021,7 +1032,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); quickPick.onDidAccept(() => { const result = quickPick.activeItems[0]; - this.sendText(type === 'cwd' ? `cd ${result.label}` : result.label, !quickPick.keyMods.alt); + this.sendText(type === 'cwd' ? `cd ${result.rawLabel}` : result.rawLabel, !quickPick.keyMods.alt, true); quickPick.hide(); }); if (value) { @@ -1486,7 +1497,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.xterm.raw.paste(currentText); } - async sendText(text: string, addNewLine: boolean): Promise<void> { + async sendText(text: string, addNewLine: boolean, bracketedPasteMode?: boolean): Promise<void> { + // Apply bracketed paste sequences if the terminal has the mode enabled, this will prevent + // the text from triggering keybindings and ensure new lines are handled properly + if (bracketedPasteMode && this.xterm?.raw.modes.bracketedPasteMode) { + text = `\x1b[200~${text}\x1b[201~`; + } + // Normalize line endings to 'enter' press. text = text.replace(/\r?\n/g, '\r'); if (addNewLine && text[text.length - 1] !== '\r') { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts b/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts index edd1bf0aeda..dac305a6c63 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts @@ -46,27 +46,28 @@ export class TerminalMainContribution extends Disposable implements IWorkbenchCo priority: RegisteredEditorPriority.exclusive }, { - canHandleDiff: false, canSupportResource: uri => uri.scheme === Schemas.vscodeTerminal, singlePerResource: true }, - ({ resource, options }) => { - const instance = terminalService.getInstanceFromResource(resource); - if (instance) { - const sourceGroup = terminalGroupService.getGroupForInstance(instance); - sourceGroup?.removeInstance(instance); - } - const resolvedResource = terminalEditorService.resolveResource(instance || resource); - const editor = terminalEditorService.getInputFromResource(resolvedResource) || { editor: resolvedResource }; - return { - editor, - options: { - ...options, - pinned: true, - forceReload: true, - override: terminalEditorId + { + createEditorInput: ({ resource, options }) => { + const instance = terminalService.getInstanceFromResource(resource); + if (instance) { + const sourceGroup = terminalGroupService.getGroupForInstance(instance); + sourceGroup?.removeInstance(instance); } - }; + const resolvedResource = terminalEditorService.resolveResource(instance || resource); + const editor = terminalEditorService.getInputFromResource(resolvedResource) || { editor: resolvedResource }; + return { + editor, + options: { + ...options, + pinned: true, + forceReload: true, + override: terminalEditorId + } + }; + } } ); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 14890b1b9bf..d262afd51fb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -71,7 +71,6 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> { @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService, @ITerminalService private readonly _terminalService: ITerminalService, @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @IInstantiationService instantiationService: IInstantiationService, @@ -103,7 +102,7 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> { listService, themeService, _configurationService, - keybindingService, + instantiationService, ); const instanceDisposables: IDisposable[] = [ @@ -308,7 +307,7 @@ class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminal } } - const shellIntegrationString = getShellIntegrationTooltip(instance, true, this._configurationService); + const shellIntegrationString = getShellIntegrationTooltip(instance, true); const iconId = this._instantiationService.invokeFunction(getIconId, instance); const hasActionbar = !this.shouldHideActionBar(); let label: string = ''; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts index 5b4bdf420e2..4b7b97e5d06 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts @@ -6,13 +6,8 @@ import { localize } from 'vs/nls'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -export function getShellIntegrationTooltip(instance: ITerminalInstance, markdown: boolean, configurationService: IConfigurationService): string { - if (!configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled) || instance.disableShellIntegrationReporting) { - return ''; - } +export function getShellIntegrationTooltip(instance: ITerminalInstance, markdown: boolean): string { const shellIntegrationCapabilities: TerminalCapability[] = []; if (instance.capabilities.has(TerminalCapability.CommandDetection)) { shellIntegrationCapabilities.push(TerminalCapability.CommandDetection); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index bb3b8225a5f..81916e856ef 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -112,11 +112,11 @@ export class TerminalViewPane extends ViewPane { this._terminalTabbedView?.rerenderTabs(); } })); - _configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationsEnabled) || e.affectsConfiguration(TerminalSettingId.ShellIntegrationEnabled)) { - this._updateForShellIntegration(); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (this._parentDomElement && (e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationsEnabled) || e.affectsConfiguration(TerminalSettingId.ShellIntegrationEnabled))) { + this._updateForShellIntegration(this._parentDomElement); } - }); + })); this._register(this._terminalService.onDidCreateInstance((i) => { i.capabilities.onDidAddCapability(c => { if (c === TerminalCapability.CommandDetection && !this._gutterDecorationsEnabled()) { @@ -124,15 +124,10 @@ export class TerminalViewPane extends ViewPane { } }); })); - this._updateForShellIntegration(); } - private _updateForShellIntegration() { - if (this._gutterDecorationsEnabled()) { - this._parentDomElement?.classList.add('shell-integration'); - } else { - this._parentDomElement?.classList.remove('shell-integration'); - } + private _updateForShellIntegration(container: HTMLElement) { + container.classList.toggle('shell-integration', this._gutterDecorationsEnabled()); } private _gutterDecorationsEnabled(): boolean { @@ -143,6 +138,9 @@ export class TerminalViewPane extends ViewPane { override renderBody(container: HTMLElement): void { super.renderBody(container); + if (!this._parentDomElement) { + this._updateForShellIntegration(container); + } this._parentDomElement = container; this._parentDomElement.classList.add('integrated-terminal'); this._fontStyleElement = document.createElement('style'); @@ -397,7 +395,7 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { { id: action.id, title: _instantiationService.invokeFunction(getSingleTabLabel, _terminalGroupService.activeInstance, _terminalService.configHelper.config.tabs.separator), - tooltip: getSingleTabTooltip(_terminalGroupService.activeInstance, _terminalService.configHelper.config.tabs.separator, configurationService) + tooltip: getSingleTabTooltip(_terminalGroupService.activeInstance, _terminalService.configHelper.config.tabs.separator) }, { id: TerminalCommandId.Split, @@ -419,12 +417,12 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { this._register(this._terminalService.onDidChangeInstanceColor(e => this.updateLabel(e))); this._register(this._terminalService.onDidChangeInstanceTitle(e => { if (e === this._terminalGroupService.activeInstance) { - this._action.tooltip = getSingleTabTooltip(e, this._terminalService.configHelper.config.tabs.separator, configurationService); + this._action.tooltip = getSingleTabTooltip(e, this._terminalService.configHelper.config.tabs.separator); this.updateLabel(); } })); this._register(this._terminalService.onDidChangeInstanceCapability(e => { - this._action.tooltip = getSingleTabTooltip(e, this._terminalService.configHelper.config.tabs.separator, configurationService); + this._action.tooltip = getSingleTabTooltip(e, this._terminalService.configHelper.config.tabs.separator); this.updateLabel(e); })); @@ -549,11 +547,11 @@ function getSingleTabLabel(accessor: ServicesAccessor, instance: ITerminalInstan return `${label} $(${primaryStatus.icon.id})`; } -function getSingleTabTooltip(instance: ITerminalInstance | undefined, separator: string, configurationService: IConfigurationService): string { +function getSingleTabTooltip(instance: ITerminalInstance | undefined, separator: string): string { if (!instance) { return ''; } - const shellIntegrationString = getShellIntegrationTooltip(instance, false, configurationService); + const shellIntegrationString = getShellIntegrationTooltip(instance, false); const title = getSingleTabTitle(instance, separator); return shellIntegrationString ? title + shellIntegrationString : title; } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts index 83de46d909f..7b4caa70342 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts @@ -73,6 +73,8 @@ export class CommandNavigationAddon extends Disposable implements ICommandTracke // Clear the current marker so successive focus/selection actions are performed from the // bottom of the buffer this._currentMarker = Boundary.Bottom; + this._navigationDecoration?.dispose(); + this._navigationDecoration = undefined; this._selectionStart = null; } @@ -110,6 +112,7 @@ export class CommandNavigationAddon extends Disposable implements ICommandTracke if (markerIndex < 0) { this._currentMarker = Boundary.Top; this._terminal.scrollToTop(); + this.clearMarker(); return; } @@ -151,6 +154,7 @@ export class CommandNavigationAddon extends Disposable implements ICommandTracke if (markerIndex >= this._getCommandMarkers().length) { this._currentMarker = Boundary.Bottom; this._terminal.scrollToBottom(); + this.clearMarker(); return; } @@ -178,12 +182,14 @@ export class CommandNavigationAddon extends Disposable implements ICommandTracke }); this._navigationDecoration = decoration; if (decoration) { - const isRendered = false; + let renderedElement: HTMLElement | undefined; + decoration.onRender(element => { - if (!isRendered) { - // TODO: Remove when https://github.com/xtermjs/xterm.js/issues/3686 is fixed - if (!element.classList.contains('xterm-decoration-overview-ruler')) { - element.classList.add('terminal-scroll-highlight'); + if (!renderedElement) { + renderedElement = element; + element.classList.add('terminal-scroll-highlight', 'terminal-scroll-highlight-outline'); + if (this._terminal?.element) { + element.style.marginLeft = `-${getComputedStyle(this._terminal.element).paddingLeft}`; } } }); @@ -194,7 +200,9 @@ export class CommandNavigationAddon extends Disposable implements ICommandTracke }); // Number picked to align with symbol highlight in the editor timeout(350).then(() => { - decoration.dispose(); + if (renderedElement) { + renderedElement.classList.remove('terminal-scroll-highlight-outline'); + } }); } } diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts index 225dd72bbdc..aa801c53f36 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts @@ -7,7 +7,7 @@ import { deepStrictEqual } from 'assert'; import { Schemas } from 'vs/base/common/network'; import { OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { ITextEditorSelection, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IFileService, IFileStatWithPartialMetadata } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -27,6 +27,7 @@ import { Terminal } from 'xterm'; export interface ITerminalLinkActivationResult { source: 'editor' | 'search'; link: string; + selection?: ITextEditorSelection; } class TestCommandDetectionCapability extends CommandDetectionCapability { @@ -79,6 +80,10 @@ suite('Workbench - TerminalLinkOpeners', () => { source: 'editor', link: editor.resource?.toString() }; + // Only assert on selection if it's not the default value + if (editor.options?.selection && (editor.options.selection.startColumn !== 1 || editor.options.selection.startLineNumber !== 1)) { + activationResult.selection = editor.options.selection; + } } } as Partial<IEditorService>); // /*editorServiceSpy = */instantiationService.spy(IEditorService, 'openEditor'); @@ -212,6 +217,28 @@ suite('Workbench - TerminalLinkOpeners', () => { source: 'search' }); }); + + test('should extract line and column from links in a workspace containing spaces', async () => { + localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Linux); + const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); + opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, Promise.resolve('/space folder'), localFileOpener, localFolderOpener, OperatingSystem.Linux); + fileService.setFiles([ + URI.from({ scheme: Schemas.file, path: '/space folder/foo/bar.txt' }) + ]); + await opener.open({ + text: './foo/bar.txt:10:5', + bufferRange: { start: { x: 1, y: 1 }, end: { x: 8, y: 1 } }, + type: TerminalBuiltinLinkType.Search + }); + deepStrictEqual(activationResult, { + link: 'file:///space%20folder/foo/bar.txt', + source: 'editor', + selection: { + startColumn: 5, + startLineNumber: 10 + }, + }); + }); }); suite('Windows', () => { @@ -261,6 +288,41 @@ suite('Workbench - TerminalLinkOpeners', () => { source: 'search' }); }); + + test('should extract line and column from links in a workspace containing spaces', async () => { + localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Windows); + const localFolderOpener = instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener); + opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, Promise.resolve('/space folder'), localFileOpener, localFolderOpener, OperatingSystem.Windows); + fileService.setFiles([ + URI.from({ scheme: Schemas.file, path: '/space folder/foo/bar.txt' }) + ]); + await opener.open({ + text: './foo/bar.txt:10:5', + bufferRange: { start: { x: 1, y: 1 }, end: { x: 8, y: 1 } }, + type: TerminalBuiltinLinkType.Search + }); + deepStrictEqual(activationResult, { + link: 'file:///space%20folder/foo/bar.txt', + source: 'editor', + selection: { + startColumn: 5, + startLineNumber: 10 + }, + }); + await opener.open({ + text: '.\\foo\\bar.txt:10:5', + bufferRange: { start: { x: 1, y: 1 }, end: { x: 8, y: 1 } }, + type: TerminalBuiltinLinkType.Search + }); + deepStrictEqual(activationResult, { + link: 'file:///space%20folder/foo/bar.txt', + source: 'editor', + selection: { + startColumn: 5, + startLineNumber: 10 + }, + }); + }); }); }); }); diff --git a/src/vs/workbench/contrib/update/browser/update.contribution.ts b/src/vs/workbench/contrib/update/browser/update.contribution.ts index 1ff72278d33..78296bf7b6e 100644 --- a/src/vs/workbench/contrib/update/browser/update.contribution.ts +++ b/src/vs/workbench/contrib/update/browser/update.contribution.ts @@ -7,13 +7,16 @@ import 'vs/platform/update/common/update.config.contribution'; import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; +import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { SyncActionDescriptor, MenuRegistry, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { ShowCurrentReleaseNotesAction, ProductContribution, UpdateContribution, CheckForVSCodeUpdateAction, CONTEXT_UPDATE_STATE, SwitchProductQualityContribution } from 'vs/workbench/contrib/update/browser/update'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import product from 'vs/platform/product/common/product'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { isWindows } from 'vs/base/common/platform'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; const workbench = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench); @@ -93,3 +96,37 @@ if (ShowCurrentReleaseNotesAction.AVAILABE) { order: 5 }); } + +if (isWindows) { + class DeveloperApplyUpdateAction extends Action2 { + constructor() { + super({ + id: '_update.applyupdate', + title: { value: localize('applyUpdate', "Apply Update..."), original: 'Apply Update...' }, + category: CATEGORIES.Developer, + f1: true, + precondition: CONTEXT_UPDATE_STATE.isEqualTo(StateType.Idle) + }); + } + + async run(accessor: ServicesAccessor): Promise<void> { + const updateService = accessor.get(IUpdateService); + const fileDialogService = accessor.get(IFileDialogService); + + const updatePath = await fileDialogService.showOpenDialog({ + title: localize('pickUpdate', "Apply Update"), + filters: [{ name: 'Setup', extensions: ['exe'] }], + canSelectFiles: true, + openLabel: mnemonicButtonLabel(localize({ key: 'updateButton', comment: ['&& denotes a mnemonic'] }, "&&Update")) + }); + + if (!updatePath || !updatePath[0]) { + return; + } + + await updateService._applySpecificUpdate(updatePath[0].fsPath); + } + } + + registerAction2(DeveloperApplyUpdateAction); +} diff --git a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts index dbaf96b6c27..46cea815e4c 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts @@ -8,7 +8,7 @@ import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; +import { IOpenerService, matchesScheme, OpenOptions } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -45,7 +45,7 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { @IConfigurationService private readonly _configurationService: IConfigurationService, @IWorkspaceTrustManagementService private readonly _workspaceTrustService: IWorkspaceTrustManagementService, ) { - this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) }); + this._openerService.registerValidator({ shouldOpen: (uri, options) => this.validateLink(uri, options) }); this._readAuthenticationTrustedDomainsResult = new IdleValue(() => this._instantiationService.invokeFunction(readAuthenticationTrustedDomains)); @@ -64,12 +64,12 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { }); } - async validateLink(resource: URI | string): Promise<boolean> { + async validateLink(resource: URI | string, openOptions?: OpenOptions): Promise<boolean> { if (!matchesScheme(resource, Schemas.http) && !matchesScheme(resource, Schemas.https)) { return true; } - if (this._workspaceTrustService.isWorkspaceTrusted() && !this._configurationService.getValue('workbench.trustedDomains.promptInTrustedWorkspace')) { + if (openOptions?.fromWorkspace && this._workspaceTrustService.isWorkspaceTrusted() && !this._configurationService.getValue('workbench.trustedDomains.promptInTrustedWorkspace')) { return true; } diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index 0c2b7990a49..324069eb10a 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -10,7 +10,7 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; import { Action2, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; @@ -21,15 +21,14 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; -import { IUserDataProfileManagementService, IUserDataProfileService, ManageProfilesSubMenu, PROFILES_CATEGORY, PROFILES_ENABLEMENT_CONTEXT, PROFILES_TTILE } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; - -const CONTEXT_CURRENT_PROFILE = new RawContextKey<string>('currentUserDataProfile', ''); +import { CURRENT_PROFILE_CONTEXT, HAS_PROFILES_CONTEXT, IUserDataProfileManagementService, IUserDataProfileService, ManageProfilesSubMenu, PROFILES_CATEGORY, PROFILES_ENABLEMENT_CONTEXT, PROFILES_TTILE } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export const userDataProfilesIcon = registerIcon('settingsProfiles-icon', Codicon.settings, localize('settingsProfilesIcon', 'Icon for Settings Profiles.')); export class UserDataProfilesWorkbenchContribution extends Disposable implements IWorkbenchContribution { private readonly currentProfileContext: IContextKey<string>; + private readonly hasProfilesContext: IContextKey<boolean>; constructor( @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @@ -44,12 +43,16 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.registerConfiguration(); - this.currentProfileContext = CONTEXT_CURRENT_PROFILE.bindTo(contextKeyService); + this.currentProfileContext = CURRENT_PROFILE_CONTEXT.bindTo(contextKeyService); this.currentProfileContext.set(this.userDataProfileService.currentProfile.id); this._register(this.userDataProfileService.onDidChangeCurrentProfile(e => this.currentProfileContext.set(this.userDataProfileService.currentProfile.id))); + this.hasProfilesContext = HAS_PROFILES_CONTEXT.bindTo(contextKeyService); + this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1); + this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1))); + this.updateStatus(); - this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.userDataProfileService.onDidChangeCurrentProfile, this.userDataProfilesService.onDidChangeProfiles)(() => this.updateStatus())); + this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.userDataProfileService.onDidChangeCurrentProfile, this.userDataProfileService.onDidUpdateCurrentProfile, this.userDataProfilesService.onDidChangeProfiles)(() => this.updateStatus())); this.registerActions(); } @@ -117,7 +120,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements super({ id: `workbench.profiles.actions.profileEntry.${profile.id}`, title: profile.name, - toggled: ContextKeyExpr.equals(CONTEXT_CURRENT_PROFILE.key, profile.id), + toggled: ContextKeyExpr.equals(CURRENT_PROFILE_CONTEXT.key, profile.id), menu: [ { id: ManageProfilesSubMenu, diff --git a/src/vs/workbench/contrib/userDataProfile/common/userDataProfileActions.ts b/src/vs/workbench/contrib/userDataProfile/common/userDataProfileActions.ts index 01a3ef3d346..f1d27222b50 100644 --- a/src/vs/workbench/contrib/userDataProfile/common/userDataProfileActions.ts +++ b/src/vs/workbench/contrib/userDataProfile/common/userDataProfileActions.ts @@ -14,20 +14,91 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { INotificationService } from 'vs/platform/notification/common/notification'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { asJson, asText, IRequestService } from 'vs/platform/request/common/request'; -import { IUserDataProfileTemplate, isUserDataProfileTemplate, IUserDataProfileManagementService, IUserDataProfileImportExportService, PROFILES_CATEGORY, PROFILE_EXTENSION, PROFILE_FILTER, ManageProfilesSubMenu, IUserDataProfileService, PROFILES_ENABLEMENT_CONTEXT } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfileTemplate, isUserDataProfileTemplate, IUserDataProfileManagementService, IUserDataProfileImportExportService, PROFILES_CATEGORY, PROFILE_EXTENSION, PROFILE_FILTER, ManageProfilesSubMenu, IUserDataProfileService, PROFILES_ENABLEMENT_CONTEXT, HAS_PROFILES_CONTEXT } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ICommandService } from 'vs/platform/commands/common/commands'; -registerAction2(class CreateFromCurrentProfileAction extends Action2 { +class CreateFromCurrentProfileAction extends Action2 { + static readonly ID = 'workbench.profiles.actions.createFromCurrentProfile'; + static readonly TITLE = { + value: localize('save profile as', "Create from Current Settings Profile..."), + original: 'Create from Current Profile...' + }; constructor() { super({ - id: 'workbench.profiles.actions.createFromCurrentProfile', + id: CreateFromCurrentProfileAction.ID, + title: CreateFromCurrentProfileAction.TITLE, + category: PROFILES_CATEGORY, + f1: true, + precondition: PROFILES_ENABLEMENT_CONTEXT + }); + } + + async run(accessor: ServicesAccessor) { + const quickInputService = accessor.get(IQuickInputService); + const notificationService = accessor.get(INotificationService); + const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService); + const name = await quickInputService.input({ + placeHolder: localize('name', "Profile name"), + title: localize('save profile as', "Create from Current Settings Profile..."), + }); + if (name) { + try { + await userDataProfileManagementService.createAndEnterProfile(name, undefined, true); + } catch (error) { + notificationService.error(error); + } + } + } +} +registerAction2(CreateFromCurrentProfileAction); + +class CreateEmptyProfileAction extends Action2 { + static readonly ID = 'workbench.profiles.actions.createEmptyProfile'; + static readonly TITLE = { + value: localize('create empty profile', "Create an Empty Settings Profile..."), + original: 'Create an Empty Settings Profile...' + }; + constructor() { + super({ + id: CreateEmptyProfileAction.ID, + title: CreateEmptyProfileAction.TITLE, + category: PROFILES_CATEGORY, + f1: true, + precondition: PROFILES_ENABLEMENT_CONTEXT + }); + } + + async run(accessor: ServicesAccessor) { + const quickInputService = accessor.get(IQuickInputService); + const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService); + const notificationService = accessor.get(INotificationService); + const name = await quickInputService.input({ + placeHolder: localize('name', "Profile name"), + title: localize('create and enter empty profile', "Create an Empty Profile..."), + }); + if (name) { + try { + await userDataProfileManagementService.createAndEnterProfile(name); + } catch (error) { + notificationService.error(error); + } + } + } +} +registerAction2(CreateEmptyProfileAction); + +registerAction2(class CreateProfileAction extends Action2 { + constructor() { + super({ + id: 'workbench.profiles.actions.createProfile', title: { - value: localize('save profile as', "Create from Current Settings Profile..."), - original: 'Create from Current Profile...' + value: localize('create profile', "Create..."), + original: 'Create...' }, category: PROFILES_CATEGORY, f1: true, @@ -35,7 +106,7 @@ registerAction2(class CreateFromCurrentProfileAction extends Action2 { menu: [ { id: ManageProfilesSubMenu, - group: '1_create_profiles', + group: '2_manage_profiles', when: PROFILES_ENABLEMENT_CONTEXT, order: 1 } @@ -45,34 +116,38 @@ registerAction2(class CreateFromCurrentProfileAction extends Action2 { async run(accessor: ServicesAccessor) { const quickInputService = accessor.get(IQuickInputService); - const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService); - const name = await quickInputService.input({ - placeHolder: localize('name', "Profile name"), - title: localize('save profile as', "Create from Current Settings Profile..."), - }); - if (name) { - await userDataProfileManagementService.createAndEnterProfile(name, undefined, true); + const commandService = accessor.get(ICommandService); + const pick = await quickInputService.pick( + [{ + id: CreateFromCurrentProfileAction.ID, + label: CreateFromCurrentProfileAction.TITLE.value, + }, { + id: CreateEmptyProfileAction.ID, + label: CreateEmptyProfileAction.TITLE.value, + }], { canPickMany: false, title: localize('create settings profile', "{0}: Create...", PROFILES_CATEGORY) }); + if (pick) { + return commandService.executeCommand(pick.id); } } }); -registerAction2(class CreateEmptyProfileAction extends Action2 { +registerAction2(class RenameProfileAction extends Action2 { constructor() { super({ - id: 'workbench.profiles.actions.createProfile', + id: 'workbench.profiles.actions.renameProfile', title: { - value: localize('create profile', "Create an Empty Settings Profile..."), - original: 'Create an Empty Profile...' + value: localize('rename profile', "Rename..."), + original: 'Rename...' }, category: PROFILES_CATEGORY, f1: true, - precondition: PROFILES_ENABLEMENT_CONTEXT, + precondition: ContextKeyExpr.and(PROFILES_ENABLEMENT_CONTEXT, HAS_PROFILES_CONTEXT), menu: [ { id: ManageProfilesSubMenu, - group: '1_create_profiles', + group: '2_manage_profiles', when: PROFILES_ENABLEMENT_CONTEXT, - order: 2 + order: 1 } ] }); @@ -80,33 +155,56 @@ registerAction2(class CreateEmptyProfileAction extends Action2 { async run(accessor: ServicesAccessor) { const quickInputService = accessor.get(IQuickInputService); + const userDataProfileService = accessor.get(IUserDataProfileService); + const userDataProfilesService = accessor.get(IUserDataProfilesService); const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService); - const name = await quickInputService.input({ - placeHolder: localize('name', "Profile name"), - title: localize('create and enter empty profile', "Create an Empty Profile..."), - }); - if (name) { - await userDataProfileManagementService.createAndEnterProfile(name); + const notificationService = accessor.get(INotificationService); + + const profiles = userDataProfilesService.profiles.filter(p => !p.isDefault); + if (profiles.length) { + const pick = await quickInputService.pick( + profiles.map(profile => ({ + label: profile.name, + description: profile.id === userDataProfileService.currentProfile.id ? localize('current', "Current") : undefined, + profile + })), + { + placeHolder: localize('pick profile to rename', "Select Settings Profile to Rename"), + }); + if (pick) { + const name = await quickInputService.input({ + value: pick.profile.name, + title: localize('edit settings profile', "Rename Settings Profile..."), + }); + if (name && name !== pick.profile.name) { + try { + await userDataProfileManagementService.renameProfile(pick.profile, name); + } catch (error) { + notificationService.error(error); + } + } + } } } }); -registerAction2(class RemoveProfileAction extends Action2 { +registerAction2(class DeleteProfileAction extends Action2 { constructor() { super({ - id: 'workbench.profiles.actions.removeProfile', + id: 'workbench.profiles.actions.deleteProfile', title: { - value: localize('remove profile', "Remove Settings Profile..."), - original: 'Remove Profile...' + value: localize('delete profile', "Delete..."), + original: 'Delete...' }, category: PROFILES_CATEGORY, f1: true, - precondition: PROFILES_ENABLEMENT_CONTEXT, + precondition: ContextKeyExpr.and(PROFILES_ENABLEMENT_CONTEXT, HAS_PROFILES_CONTEXT), menu: [ { id: ManageProfilesSubMenu, group: '2_manage_profiles', - when: PROFILES_ENABLEMENT_CONTEXT + when: PROFILES_ENABLEMENT_CONTEXT, + order: 2 } ] }); @@ -119,12 +217,21 @@ registerAction2(class RemoveProfileAction extends Action2 { const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService); const notificationService = accessor.get(INotificationService); - const profiles = userDataProfilesService.profiles.filter(p => p.id !== userDataProfileService.currentProfile.id && !p.isDefault); + const profiles = userDataProfilesService.profiles.filter(p => !p.isDefault); if (profiles.length) { - const pick = await quickInputService.pick(profiles.map(profile => ({ label: profile.name, profile })), { placeHolder: localize('pick profile', "Select Settings Profile") }); - if (pick) { + const picks = await quickInputService.pick( + profiles.map(profile => ({ + label: profile.name, + description: profile.id === userDataProfileService.currentProfile.id ? localize('current', "Current") : undefined, + profile + })), + { + placeHolder: localize('pick profile to delete', "Select Settings Profiles to Delete"), + canPickMany: true + }); + if (picks) { try { - await userDataProfileManagementService.removeProfile(pick.profile); + await Promise.all(picks.map(pick => userDataProfileManagementService.removeProfile(pick.profile))); } catch (error) { notificationService.error(error); } @@ -138,12 +245,12 @@ registerAction2(class SwitchProfileAction extends Action2 { super({ id: 'workbench.profiles.actions.switchProfile', title: { - value: localize('switch profile', "Switch Settings Profile..."), - original: 'Switch Settings Profile...' + value: localize('switch profile', "Switch..."), + original: 'Switch...' }, category: PROFILES_CATEGORY, f1: true, - precondition: PROFILES_ENABLEMENT_CONTEXT, + precondition: ContextKeyExpr.and(PROFILES_ENABLEMENT_CONTEXT, HAS_PROFILES_CONTEXT), }); } @@ -198,8 +305,8 @@ registerAction2(class ExportProfileAction extends Action2 { super({ id: 'workbench.profiles.actions.exportProfile', title: { - value: localize('export profile', "Export Settings Profile..."), - original: 'Export Settings Profile...' + value: localize('export profile', "Export..."), + original: 'Export...' }, category: PROFILES_CATEGORY, menu: [ @@ -243,8 +350,8 @@ registerAction2(class ImportProfileAction extends Action2 { super({ id: 'workbench.profiles.actions.importProfile', title: { - value: localize('import profile', "Import Settings Profile..."), - original: 'Import Settings Profile...' + value: localize('import profile', "Import..."), + original: 'Import...' }, category: PROFILES_CATEGORY, menu: [ diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 9f113cfabab..d201a99a8a6 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -18,7 +18,7 @@ import { localize } from 'vs/nls'; import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyEqualsExpr, ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -57,9 +57,8 @@ import { IUserDataInitializationService } from 'vs/workbench/services/userData/b import { MarkdownString } from 'vs/base/common/htmlContent'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ctxIsMergeEditor } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { ctxIsMergeEditor, ctxMergeBaseUri } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; const CONTEXT_CONFLICTS_SOURCES = new RawContextKey<string>('conflictsSources', ''); @@ -729,14 +728,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo for (const conflict of conflicts) { const remoteResourceName = localize({ key: 'remoteResourceName', comment: ['remote as in file in cloud'] }, "{0} (Remote)", basename(conflict.remoteResource)); const localResourceName = localize('localResourceName', "{0} (Local)", basename(conflict.remoteResource)); - const input = this.instantiationService.createInstance( - MergeEditorInput, - conflict.baseResource, - { title: localize('Yours', 'Yours'), description: localResourceName, detail: undefined, uri: conflict.localResource }, - { title: localize('Theirs', 'Theirs'), description: remoteResourceName, detail: undefined, uri: conflict.remoteResource }, - conflict.previewResource, - ); - await this.editorService.openEditor(input); + await this.editorService.openEditor({ + input1: { resource: conflict.remoteResource, label: localize('Theirs', 'Theirs'), description: remoteResourceName }, + input2: { resource: conflict.localResource, label: localize('Yours', 'Yours'), description: localResourceName }, + base: { resource: conflict.baseResource }, + result: { resource: conflict.previewResource } + }); } } @@ -1291,7 +1288,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo title: localize('accept merges title', "Accept Merge"), menu: [{ id: MenuId.MergeToolbar, - when: ContextKeyExpr.and(ctxIsMergeEditor, ContextKeyEqualsExpr.create('baseResourceScheme', USER_DATA_SYNC_SCHEME)), + when: ContextKeyExpr.and(ctxIsMergeEditor, ContextKeyExpr.regex(ctxMergeBaseUri.key, new RegExp(`^${USER_DATA_SYNC_SCHEME}:`))), }], }); } diff --git a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts index d9cb718cefc..81499a931b6 100644 --- a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts +++ b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts @@ -44,6 +44,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { private _findWidgetEnabled: IContextKey<boolean> | undefined; public readonly id: string; + public readonly providedId?: string; public readonly origin: string; public constructor( @@ -55,6 +56,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { super(); this.id = initInfo.id; + this.providedId = initInfo.providedId; this.origin = initInfo.origin ?? generateUuid(); this._extension = initInfo.extension; @@ -192,6 +194,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { if (!this._webview.value) { const webview = this._webviewService.createWebviewElement({ id: this.id, + providedId: this.providedId, origin: this.origin, options: this._options, contentOptions: this._contentOptions, diff --git a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index 6469cf31e74..8bba582ed17 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -1053,9 +1053,33 @@ } e.preventDefault(); + + let context = {}; + + /** @type HTMLElement */ + let el = e.target; + while (true) { + if (!el) { + break; + } + + // Search self/ancestors for the closest context data attribute + el = el.closest('[data-vscode-context]'); + if (!el) { + break; + } + + try { + context = { ...JSON.parse(el.getAttribute('data-vscode-context')), ...context }; + } catch {} + + el = el.parentElement; + } + hostMessaging.postMessage('did-context-menu', { clientX: e.clientX, clientY: e.clientY, + context: context }); }); diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 965b90ace22..52276ac415c 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,7 @@ <meta charset="UTF-8"> <meta http-equiv="Content-Security-Policy" - content="default-src 'none'; script-src 'sha256-vGloSX/Mg/JYMjFOA5bYxbKTao1iYLW/tlq9ME/cEOo=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> + content="default-src 'none'; script-src 'sha256-31a+qtoOhPC136ibtip6gpqSiW72yo48kmtxyi0Qptc=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> <!-- Disable pinch zooming --> <meta name="viewport" @@ -1054,9 +1054,33 @@ } e.preventDefault(); + + let context = {}; + + /** @type HTMLElement */ + let el = e.target; + while (true) { + if (!el) { + break; + } + + // Search self/ancestors for the closest context data attribute + el = el.closest('[data-vscode-context]'); + if (!el) { + break; + } + + try { + context = { ...JSON.parse(el.getAttribute('data-vscode-context')), ...context }; + } catch {} + + el = el.parentElement; + } + hostMessaging.postMessage('did-context-menu', { clientX: e.clientX, clientY: e.clientY, + context: context }); }); diff --git a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts index 098534ace64..0f13a054471 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts @@ -7,6 +7,7 @@ import { MultiCommand, RedoCommand, SelectAllCommand, UndoCommand } from 'vs/edi import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/browser/clipboard'; import * as nls from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IWebviewService, IWebview } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -44,13 +45,17 @@ overrideCommandForWebview(CopyAction, webview => webview.copy()); overrideCommandForWebview(PasteAction, webview => webview.paste()); overrideCommandForWebview(CutAction, webview => webview.cut()); +export const PreventDefaultContextMenuItemsContextKeyName = 'preventDefaultContextMenuItems'; + if (CutAction) { MenuRegistry.appendMenuItem(MenuId.WebviewContext, { command: { id: CutAction.id, title: nls.localize('cut', "Cut"), }, + group: '5_cutcopypaste', order: 1, + when: ContextKeyExpr.not(PreventDefaultContextMenuItemsContextKeyName), }); } @@ -60,7 +65,9 @@ if (CopyAction) { id: CopyAction.id, title: nls.localize('copy', "Copy"), }, + group: '5_cutcopypaste', order: 2, + when: ContextKeyExpr.not(PreventDefaultContextMenuItemsContextKeyName), }); } @@ -70,6 +77,8 @@ if (PasteAction) { id: PasteAction.id, title: nls.localize('paste', "Paste"), }, + group: '5_cutcopypaste', order: 3, + when: ContextKeyExpr.not(PreventDefaultContextMenuItemsContextKeyName), }); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index fcc0777e76a..465ab3b4681 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -102,6 +102,7 @@ namespace WebviewState { export interface WebviewInitInfo { readonly id: string; + readonly providedId?: string; readonly origin?: string; readonly options: WebviewOptions; @@ -110,6 +111,10 @@ export interface WebviewInitInfo { readonly extension: WebviewExtensionDescription | undefined; } +interface WebviewActionContext { + webview?: string; + [key: string]: unknown; +} export class WebviewElement extends Disposable implements IWebview, WebviewFindDelegate { @@ -118,6 +123,12 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD */ public readonly id: string; + + /** + * The provided identifier of this webview. + */ + public readonly providedId?: string; + /** * The origin this webview itself is loaded from. May not be unique */ @@ -199,6 +210,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD super(); this.id = initInfo.id; + this.providedId = initInfo.providedId; this.iframeId = generateUuid(); this.origin = initInfo.origin ?? this.iframeId; @@ -319,7 +331,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this.handleKeyEvent('keyup', data); })); - this._register(this.on(WebviewMessageChannels.didContextMenu, (data: { clientX: number; clientY: number }) => { + this._register(this.on(WebviewMessageChannels.didContextMenu, (data: { clientX: number; clientY: number; context: { [key: string]: unknown } }) => { if (!this.element) { return; } @@ -329,12 +341,18 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD const elementBox = this.element.getBoundingClientRect(); contextMenuService.showContextMenu({ getActions: () => { + const contextKeyService = this._contextKeyService!.createOverlay([ + ...Object.entries(data.context), + ['webview', this.providedId], + ]); + const result: IAction[] = []; - const menu = menuService.createMenu(MenuId.WebviewContext, this._contextKeyService!); - createAndFillInContextMenuActions(menu, undefined, result); + const menu = menuService.createMenu(MenuId.WebviewContext, contextKeyService); + createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result); menu.dispose(); return result; }, + getActionsContext: (): WebviewActionContext => ({ ...data.context, webview: this.providedId }), getAnchor: () => ({ x: elementBox.x + data.clientX, y: elementBox.y + data.clientY @@ -560,7 +578,12 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } protected webviewContentEndpoint(encodedWebviewOrigin: string): string { - const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', encodedWebviewOrigin); + const webviewExternalEndpoint = this._environmentService.webviewExternalEndpoint; + if (!webviewExternalEndpoint) { + throw new Error(`'webviewExternalEndpoint' has not been configured. Webviews will not work!`); + } + + const endpoint = webviewExternalEndpoint.replace('{{uuid}}', encodedWebviewOrigin); if (endpoint[endpoint.length - 1] === '/') { return endpoint.slice(0, endpoint.length - 1); } diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index 37b194916ec..9a149dca7ee 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -169,6 +169,7 @@ export class WebviewViewPane extends ViewPane { const webviewId = generateUuid(); const webview = this.webviewService.createWebviewOverlay({ id: webviewId, + providedId: this.id, options: { purpose: WebviewContentPurpose.WebviewView }, contentOptions: {}, extension: this.extensionId ? { id: this.extensionId } : undefined diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 210f171d0a8..1cdd953a342 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -435,7 +435,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ } extension.contributes?.walkthroughs?.forEach(section => { - const categoryID = extension.identifier.value + '#walkthrough#' + section.id; + const categoryID = extension.identifier.value + '#' + section.id; section.steps.forEach(step => { const fullyQualifiedID = extension.identifier.value + '#' + section.id + '#' + step.id; this.steps.delete(fullyQualifiedID); diff --git a/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts b/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts index 9bbe2ed1d27..9dc61c27381 100644 --- a/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts +++ b/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts @@ -101,7 +101,9 @@ class NewFileTemplatesManager extends Disposable { const disposables = new DisposableStore(); const qp = this.quickInputService.createQuickPick(); - qp.title = localize('createNew', "Create New..."); + qp.title = localize('selectFileType', "Select File Type..."); + qp.placeholder = qp.title; + qp.sortByLabel = false; qp.matchOnDetail = true; qp.matchOnDescription = true; @@ -168,8 +170,8 @@ class NewFileTemplatesManager extends Disposable { return; } const currentTextEntry: NewFileItem = { - commandID: 'workbench.action.files.newUntitledFile', - commandArgs: { languageId: undefined, viewType: undefined, path: val }, + commandID: 'workbench.action.files.newFile', + commandArgs: { languageId: undefined, viewType: undefined, fileName: val }, title: localize('miNewFileWithName', "New File ({0})", val), group: 'file', from: builtInSource, diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 4720f73a32a..f6f5b60e313 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -207,7 +207,22 @@ export class TitlebarPart extends BrowserTitleBarPart { } const zoomFactor = getZoomFactor(); - this.onContextMenu(new MouseEvent('mouseup', { clientX: x / zoomFactor, clientY: y / zoomFactor }), MenuId.TitleBarContext); + const boundingRect = this.rootContainer.getBoundingClientRect(); + const eventPosition = { x, y }; + const relativeCoordinates = { x, y }; + // When comparing the coordinates with the title bar, account for zoom level if not using counter zoom. + if (!this.useCounterZoom) { + relativeCoordinates.x /= zoomFactor; + relativeCoordinates.y /= zoomFactor; + } + + // Don't trigger the menu if the click is not over the title bar + if (relativeCoordinates.x < boundingRect.left || relativeCoordinates.x > boundingRect.right || + relativeCoordinates.y < boundingRect.top || relativeCoordinates.y > boundingRect.bottom) { + return; + } + + this.onContextMenu(new MouseEvent('mouseup', { clientX: eventPosition.x / zoomFactor, clientY: eventPosition.y / zoomFactor }), MenuId.TitleBarContext); })); } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index f044363676a..54779255d79 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -65,6 +65,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; import { ILabelService } from 'vs/platform/label/common/label'; import { dirname } from 'vs/base/common/resources'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; export class NativeWindow extends Disposable { @@ -115,7 +116,8 @@ export class NativeWindow extends Disposable { @IInstantiationService private readonly instantiationService: IInstantiationService, @ISharedProcessService private readonly sharedProcessService: ISharedProcessService, @IProgressService private readonly progressService: IProgressService, - @ILabelService private readonly labelService: ILabelService + @ILabelService private readonly labelService: ILabelService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { super(); @@ -129,7 +131,15 @@ export class NativeWindow extends Disposable { this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e, true))); // React to editor input changes - this._register(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu())); + const appRootUri = URI.file(this.environmentService.appRoot); + this._register(this.editorService.onDidActiveEditorChange(() => { + + // Touchbar + this.updateTouchbarMenu(); + + // Potential data loss + this.notifyOnAppRootEditors(appRootUri); + })); // prevent opening a real URL inside the window for (const event of [EventType.DRAG_OVER, EventType.DROP]) { @@ -795,6 +805,46 @@ export class NativeWindow extends Disposable { } } + private notifyOnAppRootEditors(appRootUri: URI): void { + const resourceUri = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.BOTH }); + const isResourceAppRootedFn = (uri: URI): boolean => this.uriIdentityService.extUri.isEqualOrParent(uri, appRootUri); + let isResourceAppRooted = false; + if (URI.isUri(resourceUri)) { + if (isResourceAppRootedFn(resourceUri)) { + isResourceAppRooted = true; + } + } else if (resourceUri) { + if (resourceUri.primary && isResourceAppRootedFn(resourceUri.primary)) { + isResourceAppRooted = true; + } else if (resourceUri.secondary && isResourceAppRootedFn(resourceUri.secondary)) { + isResourceAppRooted = true; + } + } + + // It is dangerous to edit files in the installation directory of Code because + // an update will remove all files and replace them with the new version. + // As such, we notify the user whenever an editor opens that is located somewhere + // in the installation directory. + // https://github.com/microsoft/vscode/issues/138815 + + if (isResourceAppRooted) { + this.notificationService.prompt( + Severity.Warning, + localize('notifyOnAppRootEditors', "Files within the installation folder of '{0}' ({1}) will be OVERWRITTEN or DELETED IRREVERSIBLY without warning during a future update.", this.productService.nameShort, this.environmentService.appRoot), + [{ + label: localize('understood', 'Understood'), + run: async () => { + // Nothing to do + } + }], + { + neverShowAgain: { id: 'window.notifyOnAppRootEditors', isSecondary: true }, + sticky: true + } + ); + } + } + private onAddFoldersRequest(request: IAddFoldersRequest): void { // Buffer all pending requests @@ -862,7 +912,7 @@ export class NativeWindow extends Disposable { input2: { resource: resources[1].resource }, base: { resource: resources[2].resource }, result: { resource: resources[3].resource }, - options: { pinned: true, override: 'mergeEditor.Input' } // TODO@bpasero remove the override once the resolver is ready + options: { pinned: true } }; editors.push(mergeEditor); } else if (diffMode && isResourceEditorInput(resources[0]) && isResourceEditorInput(resources[1])) { diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 001cf0f3815..b9be48d2fc1 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -272,7 +272,14 @@ const apiMenus: IAPIMenu[] = [ id: MenuId.MergeToolbar, description: localize('merge.toolbar', "The prominent botton in the merge editor"), proposed: 'contribMergeEditorToolbar' - } + }, + { + key: 'webview/context', + id: MenuId.WebviewContext, + description: localize('webview.context', "The webview context menu"), + proposed: 'contribWebviewContext' + }, + ]; namespace schema { @@ -768,7 +775,6 @@ menusExtensionPoint.setHandler(extensions => { } if (!menu) { - collector.info(localize('menuId.invalid', "`{0}` is not a valid menu identifier", entry[0])); continue; } diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 26e00008c87..8713a124b0d 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -24,6 +24,7 @@ export const folderSettingsSchemaId = 'vscode://schemas/settings/folder'; export const launchSchemaId = 'vscode://schemas/launch'; export const tasksSchemaId = 'vscode://schemas/tasks'; +export const APPLICATION_SCOPES = [ConfigurationScope.APPLICATION]; export const PROFILE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE]; export const LOCAL_MACHINE_PROFILE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE]; export const LOCAL_MACHINE_SCOPES = [ConfigurationScope.APPLICATION, ...LOCAL_MACHINE_PROFILE_SCOPES]; @@ -63,7 +64,7 @@ export type RestrictedSettings = { export const IWorkbenchConfigurationService = refineServiceDecorator<IConfigurationService, IWorkbenchConfigurationService>(IConfigurationService); export interface IWorkbenchConfigurationService extends IConfigurationService { /** - * Restricted settings defined in each configuraiton target + * Restricted settings defined in each configuration target */ readonly restrictedSettings: RestrictedSettings; diff --git a/src/vs/workbench/services/editor/browser/editorResolverService.ts b/src/vs/workbench/services/editor/browser/editorResolverService.ts index 666798d3bea..c00b46869c6 100644 --- a/src/vs/workbench/services/editor/browser/editorResolverService.ts +++ b/src/vs/workbench/services/editor/browser/editorResolverService.ts @@ -14,7 +14,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, EditorResourceAccessor, EditorInputWithOpti import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Schemas } from 'vs/base/common/network'; -import { RegisteredEditorInfo, RegisteredEditorPriority, RegisteredEditorOptions, DiffEditorInputFactoryFunction, EditorAssociation, EditorAssociations, EditorInputFactoryFunction, editorsAssociationsSettingId, globMatchesResource, IEditorResolverService, priorityToRank, ResolvedEditor, ResolvedStatus, UntitledEditorInputFactoryFunction, MergeEditorInputFactoryFunction } from 'vs/workbench/services/editor/common/editorResolverService'; +import { RegisteredEditorInfo, RegisteredEditorPriority, RegisteredEditorOptions, EditorAssociation, EditorAssociations, editorsAssociationsSettingId, globMatchesResource, IEditorResolverService, priorityToRank, ResolvedEditor, ResolvedStatus, EditorInputFactoryObject } from 'vs/workbench/services/editor/common/editorResolverService'; import { IKeyMods, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { localize } from 'vs/nls'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -33,10 +33,7 @@ interface RegisteredEditor { globPattern: string | glob.IRelativePattern; editorInfo: RegisteredEditorInfo; options?: RegisteredEditorOptions; - createEditorInput: EditorInputFactoryFunction; - createUntitledEditorInput?: UntitledEditorInputFactoryFunction; - createDiffEditorInput?: DiffEditorInputFactoryFunction; - createMergeEditorInput?: MergeEditorInputFactoryFunction; + editorFactoryObject: EditorInputFactoryObject; } type RegisteredEditors = Array<RegisteredEditor>; @@ -54,7 +51,8 @@ export class EditorResolverService extends Disposable implements IEditorResolver private static readonly conflictingDefaultsStorageID = 'editorOverrideService.conflictingDefaults'; // Data Stores - private _editors: Map<string | glob.IRelativePattern, RegisteredEditors> = new Map<string | glob.IRelativePattern, RegisteredEditors>(); + private _editors: Map<string | glob.IRelativePattern, Map<string, RegisteredEditors>> = new Map<string | glob.IRelativePattern, Map<string, RegisteredEditors>>(); + private _flattenedEditors: Map<string | glob.IRelativePattern, RegisteredEditors> = new Map(); private cache: Set<string> | undefined; constructor( @@ -72,7 +70,6 @@ export class EditorResolverService extends Disposable implements IEditorResolver // Read in the cache on statup this.cache = new Set<string>(JSON.parse(this.storageService.get(EditorResolverService.cacheStorageID, StorageScope.PROFILE, JSON.stringify([])))); this.storageService.remove(EditorResolverService.cacheStorageID, StorageScope.PROFILE); - this.convertOldAssociationFormat(); this._register(this.storageService.onWillSaveState(() => { // We want to store the glob patterns we would activate on, this allows us to know if we need to await the ext host on startup for opening a resource @@ -83,13 +80,6 @@ export class EditorResolverService extends Disposable implements IEditorResolver this.extensionService.onDidRegisterExtensions(() => { this.cache = undefined; }); - - // When the setting changes we want to ensure that it is properly converted - this._register(this.configurationService.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration(editorsAssociationsSettingId)) { - this.convertOldAssociationFormat(); - } - })); } private resolveUntypedInputAndGroup(editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined): [IUntypedEditorInput, IEditorGroup, EditorActivation | undefined] | undefined { @@ -195,10 +185,8 @@ export class EditorResolverService extends Disposable implements IEditorResolver // If no override we take the selected editor id so that matches works with the isActive check untypedEditor.options = { override: selectedEditor.editorInfo.id, ...untypedEditor.options }; - let handlesDiff = typeof selectedEditor.options?.canHandleDiff === 'function' ? selectedEditor.options.canHandleDiff() : selectedEditor.options?.canHandleDiff; - // Also check that it has a factory function or else it doesn't matter - handlesDiff = handlesDiff && selectedEditor.createDiffEditorInput !== undefined; - if (handlesDiff === false && isResourceDiffEditorInput(untypedEditor)) { + // Check if diff can be created based on prescene of factory function + if (selectedEditor.editorFactoryObject.createDiffEditorInput === undefined && isResourceDiffEditorInput(untypedEditor)) { return ResolvedStatus.NONE; } @@ -244,28 +232,33 @@ export class EditorResolverService extends Disposable implements IEditorResolver globPattern: string | glob.IRelativePattern, editorInfo: RegisteredEditorInfo, options: RegisteredEditorOptions, - createEditorInput: EditorInputFactoryFunction, - createUntitledEditorInput?: UntitledEditorInputFactoryFunction, - createDiffEditorInput?: DiffEditorInputFactoryFunction, - createMergeEditorInput?: MergeEditorInputFactoryFunction + editorFactoryObject: EditorInputFactoryObject ): IDisposable { let registeredEditor = this._editors.get(globPattern); if (registeredEditor === undefined) { - registeredEditor = []; + registeredEditor = new Map<string, RegisteredEditors>(); this._editors.set(globPattern, registeredEditor); } - const remove = insert(registeredEditor, { + + let editorsWithId = registeredEditor.get(editorInfo.id); + if (editorsWithId === undefined) { + editorsWithId = []; + } + const remove = insert(editorsWithId, { globPattern, editorInfo, options, - createEditorInput, - createUntitledEditorInput, - createDiffEditorInput, - createMergeEditorInput + editorFactoryObject }); + registeredEditor.set(editorInfo.id, editorsWithId); + this._flattenedEditors = this._flattenEditorsMap(); this._onDidChangeEditorRegistrations.fire(); return toDisposable(() => { remove(); + if (editorsWithId && editorsWithId.length === 0) { + registeredEditor?.delete(editorInfo.id); + } + this._flattenedEditors = this._flattenEditorsMap(); this._onDidChangeEditorRegistrations.fire(); }); } @@ -278,23 +271,6 @@ export class EditorResolverService extends Disposable implements IEditorResolver return matchingAssociations.filter(association => allEditors.find(c => c.editorInfo.id === association.viewType)); } - private convertOldAssociationFormat(): void { - const rawAssociations = this.configurationService.getValue<EditorAssociations | { [fileNamePattern: string]: string }>(editorsAssociationsSettingId) || []; - // If it's not an array, then it's the new format - if (!Array.isArray(rawAssociations)) { - return; - } - const newSettingObject = Object.create(null); - // Make the correctly formatted object from the array and then set that object - for (const association of rawAssociations) { - if (association.filenamePattern) { - newSettingObject[association.filenamePattern] = association.viewType; - } - } - this.logService.info(`Migrating ${editorsAssociationsSettingId}`); - this.configurationService.updateValue(editorsAssociationsSettingId, newSettingObject); - } - private getAllUserAssociations(): EditorAssociations { const inspectedEditorAssociations = this.configurationService.inspect<{ [fileNamePattern: string]: string }>(editorsAssociationsSettingId) || {}; const workspaceAssociations = inspectedEditorAssociations.workspaceValue ?? {}; @@ -318,10 +294,42 @@ export class EditorResolverService extends Disposable implements IEditorResolver } /** + * Given the nested nature of the editors map, we should merge factories of the same glob and id to make it flat + */ + private _flattenEditorsMap() { + const editors = new Map<string | glob.IRelativePattern, RegisteredEditors>(); + for (const [glob, value] of this._editors) { + const registeredEditors: RegisteredEditors = []; + for (const editors of value.values()) { + let registeredEditor: RegisteredEditor | undefined = undefined; + // Merge all editors with the same id and glob pattern together + for (const editor of editors) { + if (!registeredEditor) { + registeredEditor = { + editorInfo: editor.editorInfo, + globPattern: editor.globPattern, + options: {}, + editorFactoryObject: {} + }; + } + // Merge options and factories + registeredEditor.options = { ...registeredEditor.options, ...editor.options }; + registeredEditor.editorFactoryObject = { ...registeredEditor.editorFactoryObject, ...editor.editorFactoryObject }; + } + if (registeredEditor) { + registeredEditors.push(registeredEditor); + } + } + editors.set(glob, registeredEditors); + } + return editors; + } + + /** * Returns all editors as an array. Possible to contain duplicates */ private get _registeredEditors(): RegisteredEditors { - return flatten(Array.from(this._editors.values())); + return flatten(Array.from(this._flattenedEditors.values())); } updateUserAssociations(globPattern: string, editorID: string): void { @@ -342,7 +350,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver const userSettings = this.getAssociationsForResource(resource); const matchingEditors: RegisteredEditor[] = []; // Then all glob patterns - for (const [key, editors] of this._editors) { + for (const [key, editors] of this._flattenedEditors) { for (const editor of editors) { const foundInSettings = userSettings.find(setting => setting.viewType === editor.editorInfo.id); if ((foundInSettings && editor.editorInfo.priority !== RegisteredEditorPriority.exclusive) || globMatchesResource(key, resource)) { @@ -440,19 +448,19 @@ export class EditorResolverService extends Disposable implements IEditorResolver // If it's a merge editor we trigger the create merge editor input if (isResourceMergeEditorInput(editor)) { - if (!selectedEditor.createMergeEditorInput) { + if (!selectedEditor.editorFactoryObject.createMergeEditorInput) { return; } - const inputWithOptions = await selectedEditor.createMergeEditorInput(editor, group); + const inputWithOptions = await selectedEditor.editorFactoryObject.createMergeEditorInput(editor, group); return { editor: inputWithOptions.editor, options: inputWithOptions.options ?? options }; } // If it's a diff editor we trigger the create diff editor input if (isResourceDiffEditorInput(editor)) { - if (!selectedEditor.createDiffEditorInput) { + if (!selectedEditor.editorFactoryObject.createDiffEditorInput) { return; } - const inputWithOptions = await selectedEditor.createDiffEditorInput(editor, group); + const inputWithOptions = await selectedEditor.editorFactoryObject.createDiffEditorInput(editor, group); return { editor: inputWithOptions.editor, options: inputWithOptions.options ?? options }; } @@ -461,10 +469,10 @@ export class EditorResolverService extends Disposable implements IEditorResolver } if (isUntitledResourceEditorInput(editor)) { - if (!selectedEditor.createUntitledEditorInput) { + if (!selectedEditor.editorFactoryObject.createUntitledEditorInput) { return; } - const inputWithOptions = await selectedEditor.createUntitledEditorInput(editor, group); + const inputWithOptions = await selectedEditor.editorFactoryObject.createUntitledEditorInput(editor, group); return { editor: inputWithOptions.editor, options: inputWithOptions.options ?? options }; } @@ -482,8 +490,13 @@ export class EditorResolverService extends Disposable implements IEditorResolver } } + // If no factory is above, return flow back to caller letting them know we could not resolve it + if (!selectedEditor.editorFactoryObject.createEditorInput) { + return; + } + // Respect options passed back - const inputWithOptions = await selectedEditor.createEditorInput(editor, group); + const inputWithOptions = await selectedEditor.editorFactoryObject.createEditorInput(editor, group); options = inputWithOptions.options ?? options; const input = inputWithOptions.editor; @@ -775,7 +788,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver const cacheStorage: Set<string> = new Set<string>(); // Store just the relative pattern pieces without any path info - for (const [globPattern, contribPoint] of this._editors) { + for (const [globPattern, contribPoint] of this._flattenedEditors) { const nonOptional = !!contribPoint.find(c => c.editorInfo.priority !== RegisteredEditorPriority.option && c.editorInfo.id !== DEFAULT_EDITOR_ASSOCIATION.id); // Don't keep a cache of the optional ones as those wouldn't be opened on start anyways if (!nonOptional) { diff --git a/src/vs/workbench/services/editor/common/editorResolverService.ts b/src/vs/workbench/services/editor/common/editorResolverService.ts index 57f3e2c1508..9de7b992657 100644 --- a/src/vs/workbench/services/editor/common/editorResolverService.ts +++ b/src/vs/workbench/services/editor/common/editorResolverService.ts @@ -19,6 +19,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorInputWithOptions, EditorInputWithOptionsAndGroup, IResourceDiffEditorInput, IResourceMergeEditorInput, IUntitledTextResourceEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { PreferredGroup } from 'vs/workbench/services/editor/common/editorService'; +import { AtLeastOne } from 'vs/base/common/types'; export const IEditorResolverService = createDecorator<IEditorResolverService>('editorResolverService'); @@ -84,10 +85,6 @@ export type RegisteredEditorOptions = { * If your editor cannot be opened in multiple groups for the same resource */ singlePerResource?: boolean | (() => boolean); - /** - * If your editor supports diffs - */ - canHandleDiff?: boolean | (() => boolean); /** * Whether or not you can support opening the given resource. @@ -113,6 +110,15 @@ export type DiffEditorInputFactoryFunction = (diffEditorInput: IResourceDiffEdit export type MergeEditorInputFactoryFunction = (mergeEditorInput: IResourceMergeEditorInput, group: IEditorGroup) => EditorInputFactoryResult; +type EditorInputFactories = { + createEditorInput?: EditorInputFactoryFunction; + createUntitledEditorInput?: UntitledEditorInputFactoryFunction; + createDiffEditorInput?: DiffEditorInputFactoryFunction; + createMergeEditorInput?: MergeEditorInputFactoryFunction; +}; + +export type EditorInputFactoryObject = AtLeastOne<EditorInputFactories>; + export interface IEditorResolverService { readonly _serviceBrand: undefined; /** @@ -135,20 +141,18 @@ export interface IEditorResolverService { readonly onDidChangeEditorRegistrations: Event<void>; /** - * Registers a specific editor. + * Registers a specific editor. Editors with the same glob pattern and ID will be grouped together by the resolver. + * This allows for registration of the factories in different locations * @param globPattern The glob pattern for this registration * @param editorInfo Information about the registration * @param options Specific options which apply to this registration - * @param createEditorInput The factory method for creating inputs + * @param editorFactoryObject The editor input factory functions */ registerEditor( globPattern: string | glob.IRelativePattern, editorInfo: RegisteredEditorInfo, options: RegisteredEditorOptions, - createEditorInput: EditorInputFactoryFunction, - createUntitledEditorInput?: UntitledEditorInputFactoryFunction, - createDiffEditorInput?: DiffEditorInputFactoryFunction, - createMergeEditorInput?: MergeEditorInputFactoryFunction + editorFactoryObject: EditorInputFactoryObject ): IDisposable; /** diff --git a/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts b/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts index 3158684613d..2b3a585ed5c 100644 --- a/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts @@ -40,8 +40,10 @@ suite('EditorResolverService', () => { detail: 'Test Editor Details', priority: RegisteredEditorPriority.default }, - { canHandleDiff: false }, - ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + {}, + { + createEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + } ); const resultingResolution = await service.resolveEditor({ resource: URI.file('my://resource-basics.test') }, part.activeGroup); @@ -64,9 +66,11 @@ suite('EditorResolverService', () => { detail: 'Test Editor Details', priority: RegisteredEditorPriority.default }, - { canHandleDiff: false }, - ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), - ({ resource, options }, group) => ({ editor: new TestFileEditorInput((resource ? resource : URI.from({ scheme: Schemas.untitled })), UNTITLED_TEST_EDITOR_INPUT_ID) }), + {}, + { + createEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + createUntitledEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput((resource ? resource : URI.from({ scheme: Schemas.untitled })), UNTITLED_TEST_EDITOR_INPUT_ID) }), + } ); // Untyped untitled - no resource @@ -105,8 +109,10 @@ suite('EditorResolverService', () => { detail: 'Test Editor Details Primary', priority: RegisteredEditorPriority.default }, - { canHandleDiff: false }, - ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + {}, + { + createEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + } ); const registeredEditorSecondary = service.registerEditor('*.test-secondary', @@ -116,8 +122,10 @@ suite('EditorResolverService', () => { detail: 'Test Editor Details Secondary', priority: RegisteredEditorPriority.default }, - { canHandleDiff: false }, - ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + {}, + { + createEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + } ); const resultingResolution = await service.resolveEditor({ @@ -145,18 +153,19 @@ suite('EditorResolverService', () => { detail: 'Test Editor Details', priority: RegisteredEditorPriority.default }, - { canHandleDiff: true }, - ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), - undefined, - ({ modified, original, options }, group) => ({ - editor: accessor.instantiationService.createInstance( - DiffEditorInput, - 'name', - 'description', - new TestFileEditorInput(URI.parse(original.toString()), TEST_EDITOR_INPUT_ID), - new TestFileEditorInput(URI.parse(modified.toString()), TEST_EDITOR_INPUT_ID), - undefined) - }) + {}, + { + createEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + createDiffEditorInput: ({ modified, original, options }, group) => ({ + editor: accessor.instantiationService.createInstance( + DiffEditorInput, + 'name', + 'description', + new TestFileEditorInput(URI.parse(original.toString()), TEST_EDITOR_INPUT_ID), + new TestFileEditorInput(URI.parse(modified.toString()), TEST_EDITOR_INPUT_ID), + undefined) + }) + } ); const resultingResolution = await service.resolveEditor({ @@ -186,20 +195,21 @@ suite('EditorResolverService', () => { detail: 'Test Editor Details', priority: RegisteredEditorPriority.default }, - { canHandleDiff: true }, - ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), - undefined, - ({ modified, original, options }, group) => { - diffOneCounter++; - return { - editor: accessor.instantiationService.createInstance( - DiffEditorInput, - 'name', - 'description', - new TestFileEditorInput(URI.parse(original.toString()), TEST_EDITOR_INPUT_ID), - new TestFileEditorInput(URI.parse(modified.toString()), TEST_EDITOR_INPUT_ID), - undefined) - }; + {}, + { + createEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + createDiffEditorInput: ({ modified, original, options }, group) => { + diffOneCounter++; + return { + editor: accessor.instantiationService.createInstance( + DiffEditorInput, + 'name', + 'description', + new TestFileEditorInput(URI.parse(original.toString()), TEST_EDITOR_INPUT_ID), + new TestFileEditorInput(URI.parse(modified.toString()), TEST_EDITOR_INPUT_ID), + undefined) + }; + } } ); @@ -210,20 +220,21 @@ suite('EditorResolverService', () => { detail: 'Test Editor Details', priority: RegisteredEditorPriority.default }, - { canHandleDiff: true }, - ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), - undefined, - ({ modified, original, options }, group) => { - diffTwoCounter++; - return { - editor: accessor.instantiationService.createInstance( - DiffEditorInput, - 'name', - 'description', - new TestFileEditorInput(URI.parse(original.toString()), TEST_EDITOR_INPUT_ID), - new TestFileEditorInput(URI.parse(modified.toString()), TEST_EDITOR_INPUT_ID), - undefined) - }; + {}, + { + createEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + createDiffEditorInput: ({ modified, original, options }, group) => { + diffTwoCounter++; + return { + editor: accessor.instantiationService.createInstance( + DiffEditorInput, + 'name', + 'description', + new TestFileEditorInput(URI.parse(original.toString()), TEST_EDITOR_INPUT_ID), + new TestFileEditorInput(URI.parse(modified.toString()), TEST_EDITOR_INPUT_ID), + undefined) + }; + } } ); @@ -234,20 +245,21 @@ suite('EditorResolverService', () => { detail: 'Test Editor Details', priority: RegisteredEditorPriority.option }, - { canHandleDiff: true }, - ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), - undefined, - ({ modified, original, options }, group) => { - defaultDiffCounter++; - return { - editor: accessor.instantiationService.createInstance( - DiffEditorInput, - 'name', - 'description', - new TestFileEditorInput(URI.parse(original.toString()), TEST_EDITOR_INPUT_ID), - new TestFileEditorInput(URI.parse(modified.toString()), TEST_EDITOR_INPUT_ID), - undefined) - }; + {}, + { + createEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + createDiffEditorInput: ({ modified, original, options }, group) => { + defaultDiffCounter++; + return { + editor: accessor.instantiationService.createInstance( + DiffEditorInput, + 'name', + 'description', + new TestFileEditorInput(URI.parse(original.toString()), TEST_EDITOR_INPUT_ID), + new TestFileEditorInput(URI.parse(modified.toString()), TEST_EDITOR_INPUT_ID), + undefined) + }; + } } ); @@ -354,8 +366,10 @@ suite('EditorResolverService', () => { detail: 'Test Editor Details', priority: RegisteredEditorPriority.default }, - { canHandleDiff: false }, - ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }), + {}, + { + createEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }) + } ); assert.strictEqual(eventCounter, 1); @@ -368,4 +382,67 @@ suite('EditorResolverService', () => { assert.strictEqual(service.getEditors().length, editors.length); assert.strictEqual(service.getEditors().some(editor => editor.id === 'TEST_EDITOR'), false); }); + + test('Multiple registrations to same glob and id #155859', async () => { + const [part, service, accessor] = await createEditorResolverService(); + const testEditorInfo = { + id: 'TEST_EDITOR', + label: 'Test Editor Label', + detail: 'Test Editor Details', + priority: RegisteredEditorPriority.default + }; + const registeredSingleEditor = service.registerEditor('*.test', + testEditorInfo, + {}, + { + createEditorInput: ({ resource, options }, group) => ({ editor: new TestFileEditorInput(URI.parse(resource.toString()), TEST_EDITOR_INPUT_ID) }) + } + ); + + const registeredDiffEditor = service.registerEditor('*.test', + testEditorInfo, + {}, + { + createDiffEditorInput: ({ modified, original, options }, group) => ({ + editor: accessor.instantiationService.createInstance( + DiffEditorInput, + 'name', + 'description', + new TestFileEditorInput(URI.parse(original.toString()), TEST_EDITOR_INPUT_ID), + new TestFileEditorInput(URI.parse(modified.toString()), TEST_EDITOR_INPUT_ID), + undefined) + }) + } + ); + + // Resolve a diff + let resultingResolution = await service.resolveEditor({ + original: { resource: URI.file('my://resource-basics.test') }, + modified: { resource: URI.file('my://resource-basics.test') } + }, part.activeGroup); + assert.ok(resultingResolution); + assert.notStrictEqual(typeof resultingResolution, 'number'); + if (resultingResolution !== ResolvedStatus.ABORT && resultingResolution !== ResolvedStatus.NONE) { + assert.strictEqual(resultingResolution.editor.typeId, 'workbench.editors.diffEditorInput'); + resultingResolution.editor.dispose(); + } else { + assert.fail(); + } + + // Remove diff registration + registeredDiffEditor.dispose(); + + // Resolve a diff again, expected failure + resultingResolution = await service.resolveEditor({ + original: { resource: URI.file('my://resource-basics.test') }, + modified: { resource: URI.file('my://resource-basics.test') } + }, part.activeGroup); + assert.ok(resultingResolution); + assert.strictEqual(typeof resultingResolution, 'number'); + if (resultingResolution !== ResolvedStatus.NONE) { + assert.fail(); + } + + registeredSingleEditor.dispose(); + }); }); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index f193e7d69b0..e0c3b499650 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -252,7 +252,9 @@ suite('EditorService', () => { '*.editor-service-locked-group-tests', { id: TEST_EDITOR_INPUT_ID, label: 'Label', priority: RegisteredEditorPriority.exclusive }, {}, - editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + { + createEditorInput: editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + } )); const input1: IResourceEditorInput = { resource: URI.parse('file://resource-basics.editor-service-locked-group-tests'), options: { pinned: true } }; @@ -388,7 +390,9 @@ suite('EditorService', () => { '*.editor-service-locked-group-tests', { id: TEST_EDITOR_INPUT_ID, label: 'Label', priority: RegisteredEditorPriority.exclusive }, {}, - editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + { + createEditorInput: editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + } )); const rootGroup = part.activeGroup; @@ -436,7 +440,9 @@ suite('EditorService', () => { '*.editor-service-locked-group-tests', { id: TEST_EDITOR_INPUT_ID, label: 'Label', priority: RegisteredEditorPriority.exclusive }, {}, - editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + { + createEditorInput: editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + } )); const rootGroup = part.activeGroup; @@ -484,7 +490,9 @@ suite('EditorService', () => { '*.editor-service-locked-group-tests', { id: TEST_EDITOR_INPUT_ID, label: 'Label', priority: RegisteredEditorPriority.exclusive }, {}, - editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + { + createEditorInput: editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + } )); const rootGroup = part.activeGroup; @@ -549,24 +557,26 @@ suite('EditorService', () => { disposables.add(accessor.editorResolverService.registerEditor( '*.editor-service-override-tests', { id: TEST_EDITOR_INPUT_ID, label: 'Label', priority: RegisteredEditorPriority.exclusive }, - { canHandleDiff: true }, - editor => { - editorFactoryCalled++; - lastEditorFactoryEditor = editor; - - return { editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }; - }, - untitledEditor => { - untitledEditorFactoryCalled++; - lastUntitledEditorFactoryEditor = untitledEditor; - - return { editor: new TestFileEditorInput(untitledEditor.resource ?? URI.parse(`untitled://my-untitled-editor-${untitledEditorFactoryCalled}`), TEST_EDITOR_INPUT_ID) }; - }, - diffEditor => { - diffEditorFactoryCalled++; - lastDiffEditorFactoryEditor = diffEditor; - - return { editor: new TestFileEditorInput(URI.file(`diff-editor-${diffEditorFactoryCalled}`), TEST_EDITOR_INPUT_ID) }; + {}, + { + createEditorInput: editor => { + editorFactoryCalled++; + lastEditorFactoryEditor = editor; + + return { editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }; + }, + createUntitledEditorInput: untitledEditor => { + untitledEditorFactoryCalled++; + lastUntitledEditorFactoryEditor = untitledEditor; + + return { editor: new TestFileEditorInput(untitledEditor.resource ?? URI.parse(`untitled://my-untitled-editor-${untitledEditorFactoryCalled}`), TEST_EDITOR_INPUT_ID) }; + }, + createDiffEditorInput: diffEditor => { + diffEditorFactoryCalled++; + lastDiffEditorFactoryEditor = diffEditor; + + return { editor: new TestFileEditorInput(URI.file(`diff-editor-${diffEditorFactoryCalled}`), TEST_EDITOR_INPUT_ID) }; + } } )); @@ -1401,7 +1411,9 @@ suite('EditorService', () => { '*.editor-service-override-tests', { id: TEST_EDITOR_INPUT_ID, label: 'Label', priority: RegisteredEditorPriority.exclusive }, {}, - editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + { + createEditorInput: editor => ({ editor: new TestFileEditorInput(editor.resource, TEST_EDITOR_INPUT_ID) }) + } )); // Typed editor @@ -2268,12 +2280,13 @@ suite('EditorService', () => { priority: RegisteredEditorPriority.builtin }, {}, - (editorInput) => { - editorCount++; - return ({ editor: textEditorService.createTextEditor(editorInput) }); - }, - undefined, - diffEditor => ({ editor: textEditorService.createTextEditor(diffEditor) }) + { + createEditorInput: (editorInput) => { + editorCount++; + return ({ editor: textEditorService.createTextEditor(editorInput) }); + }, + createDiffEditorInput: diffEditor => ({ editor: textEditorService.createTextEditor(diffEditor) }) + } ); assert.strictEqual(editorCount, 0); @@ -2311,12 +2324,13 @@ suite('EditorService', () => { priority: RegisteredEditorPriority.builtin }, {}, - (editorInput) => { - editorCount++; - return ({ editor: textEditorService.createTextEditor(editorInput) }); - }, - undefined, - diffEditor => ({ editor: textEditorService.createTextEditor(diffEditor) }) + { + createEditorInput: (editorInput) => { + editorCount++; + return ({ editor: textEditorService.createTextEditor(editorInput) }); + }, + createDiffEditorInput: diffEditor => ({ editor: textEditorService.createTextEditor(diffEditor) }) + } ); assert.strictEqual(editorCount, 0); @@ -2348,12 +2362,13 @@ suite('EditorService', () => { priority: RegisteredEditorPriority.builtin }, {}, - (editorInput) => { - editorCount++; - return ({ editor: textEditorService.createTextEditor(editorInput) }); - }, - undefined, - diffEditor => ({ editor: textEditorService.createTextEditor(diffEditor) }) + { + createEditorInput: (editorInput) => { + editorCount++; + return ({ editor: textEditorService.createTextEditor(editorInput) }); + }, + createDiffEditorInput: diffEditor => ({ editor: textEditorService.createTextEditor(diffEditor) }) + } ); assert.strictEqual(editorCount, 0); diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index fc9dcd1125e..00e360f20c2 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -281,7 +281,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost return { commit: this._productService.commit, version: this._productService.version, - parentPid: -1, + parentPid: 0, environment: { isExtensionDevelopmentDebug: this._environmentService.debugRenderer, appName: this._productService.nameLong, diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 61a3cfd5de2..24259163505 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -1228,10 +1228,10 @@ export abstract class AbstractExtensionService extends Disposable implements IEx type ExtensionsMessageClassification = { owner: 'alexdima'; comment: 'A validation message for an extension'; - type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - extensionPointId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - message: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Severity of problem.'; isMeasurement: true }; + extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the extension that has a problem.' }; + extensionPointId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The extension point that has a problem.' }; + message: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The message of the problem.' }; }; type ExtensionsMessageEvent = { type: Severity; @@ -1304,8 +1304,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx type ExtensionActivationErrorClassification = { owner: 'alexdima'; comment: 'An extension failed to activate'; - extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - error: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the extension.' }; + error: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The error message.' }; }; type ExtensionActivationErrorEvent = { extensionId: string; diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 7366addd3dd..2aceff7689b 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -66,12 +66,12 @@ export function createExtensionHostManager(instantiationService: IInstantiationS export type ExtensionHostStartupClassification = { owner: 'alexdima'; comment: 'The startup state of the extension host'; - time: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - action: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - kind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - errorName?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - errorMessage?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - errorStack?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + time: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The time reported by Date.now().' }; + action: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The action: starting, success or error.' }; + kind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The extension host kind: LocalProcess, LocalWebWorker or Remote.' }; + errorName?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error name.' }; + errorMessage?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error message.' }; + errorStack?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The error stack.' }; }; export type ExtensionHostStartupEvent = { diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index cc654df1787..285ab4355c2 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -20,7 +20,10 @@ export interface IExtensionDescriptionDelta { export interface IExtensionHostInitData { version: string; commit?: string; - parentPid: number; + /** + * When set to `0`, no polling for the parent process still running will happen. + */ + parentPid: number | 0; environment: IEnvironment; workspace?: IStaticWorkspaceData | null; allExtensions: IExtensionDescription[]; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index ca5260d65af..65419fce737 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -15,8 +15,10 @@ export const allApiProposals = Object.freeze({ contribMergeEditorToolbar: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts', contribRemoteHelp: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts', contribShareMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribShareMenu.d.ts', + contribViewSize: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewSize.d.ts', contribViewsRemote: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', + contribWebviewContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribWebviewContext.d.ts', customEditorMove: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', diffCommand: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', documentFiltersExclusive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', diff --git a/src/vs/workbench/services/extensions/electron-sandbox/electronExtensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/electronExtensionService.ts index 649e83699e8..34cb13522aa 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/electronExtensionService.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/electronExtensionService.ts @@ -304,9 +304,9 @@ export abstract class ElectronExtensionService extends AbstractExtensionService type ExtensionHostCrashClassification = { owner: 'alexdima'; comment: 'The extension host has terminated unexpectedly'; - code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - signal: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - extensionIds: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The exit code of the extension host process.' }; + signal: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The signal that caused the extension host process to exit.' }; + extensionIds: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The list of loaded extensions.' }; }; type ExtensionHostCrashEvent = { code: number; @@ -323,9 +323,9 @@ export abstract class ElectronExtensionService extends AbstractExtensionService type ExtensionHostCrashExtensionClassification = { owner: 'alexdima'; comment: 'The extension host has terminated unexpectedly'; - code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - signal: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The exit code of the extension host process.' }; + signal: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The signal that caused the extension host process to exit.' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The identifier of the extension.' }; }; type ExtensionHostCrashExtensionEvent = { code: number; diff --git a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index 97eeb78e28e..8d240be7670 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -437,7 +437,7 @@ export class SandboxLocalProcessExtensionHost implements IExtensionHost { return { commit: this._productService.commit, version: this._productService.version, - parentPid: process.pid, + parentPid: process.sandboxed ? 0 : process.pid, environment: { isExtensionDevelopmentDebug: this._isExtensionDevDebug, appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined, diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 0aebd31d5b9..25fca6a0136 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -268,7 +268,7 @@ export class BrowserHostService extends Disposable implements IHostService { input2: { resource: editors[1].resource }, base: { resource: editors[2].resource }, result: { resource: editors[3].resource }, - options: { pinned: true, override: 'mergeEditor.Input' } // TODO@bpasero remove the override once the resolver is ready + options: { pinned: true } }); } diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index 4135a251e94..b680533dabc 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -402,32 +402,33 @@ export class LabelService extends Disposable implements ILabelService { } private formatUri(resource: URI, formatting: ResourceLabelFormatting, forceNoTildify?: boolean): string { - let label = formatting.label.replace(labelMatchingRegexp, (match, token, qsToken, qsValue) => { - switch (token) { - case 'scheme': return resource.scheme; - case 'authority': return resource.authority; - case 'authoritySuffix': { - const i = resource.authority.indexOf('+'); - return i === -1 ? resource.authority : resource.authority.slice(i + 1); - } - case 'path': - return formatting.stripPathStartingSeparator - ? resource.path.slice(resource.path[0] === formatting.separator ? 1 : 0) - : resource.path; - default: { - if (qsToken === 'query') { - const { query } = resource; - if (query && query[0] === '{' && query[query.length - 1] === '}') { - try { - return JSON.parse(query)[qsValue] || ''; - } catch { } - } + let label = typeof formatting.label !== 'string' ? formatting.label(resource) : + formatting.label.replace(labelMatchingRegexp, (match, token, qsToken, qsValue) => { + switch (token) { + case 'scheme': return resource.scheme; + case 'authority': return resource.authority; + case 'authoritySuffix': { + const i = resource.authority.indexOf('+'); + return i === -1 ? resource.authority : resource.authority.slice(i + 1); } + case 'path': + return formatting.stripPathStartingSeparator + ? resource.path.slice(resource.path[0] === formatting.separator ? 1 : 0) + : resource.path; + default: { + if (qsToken === 'query') { + const { query } = resource; + if (query && query[0] === '{' && query[query.length - 1] === '}') { + try { + return JSON.parse(query)[qsValue] || ''; + } catch { } + } + } - return ''; + return ''; + } } - } - }); + }); // convert \c:\something => C:\something if (formatting.normalizeDriveLetter && hasDriveLetterIgnorePlatform(label)) { diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts index 745055c930e..338660d225b 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -164,6 +164,18 @@ suite('URI Label', () => { assert.strictEqual(labelService.getUriLabel(uri1, { relative: false }), 'LABEL: /END'); }); + test('custom formatting function', function () { + labelService.registerFormatter({ + scheme: 'vscode', + formatting: { + label: (resource) => { return resource.toString(); }, + separator: '/', + } + }); + + const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); + assert.strictEqual(labelService.getUriLabel(uri1), uri1.toString()); + }); test('label caching', () => { const m = new Memento('cachedResourceLabelFormatters', storageService).getMemento(StorageScope.PROFILE, StorageTarget.MACHINE); diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts index 4eebf46840b..cf5d6749fa6 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts @@ -204,9 +204,10 @@ export class LanguageDetectionWorkerHost { type LanguageDetectionStats = { languages: string; confidences: string; timeSpent: number }; type LanguageDetectionStatsClassification = { owner: 'TylerLeonhardt'; - languages: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - confidences: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - timeSpent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + comment: 'Helps understand how effective language detection is via confidences and how long it takes to run'; + languages: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The languages that are guessed' }; + confidences: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The confidences of each language guessed' }; + timeSpent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The time it took to run language detection' }; }; this._telemetryService.publicLog2<LanguageDetectionStats, LanguageDetectionStatsClassification>('automaticlanguagedetection.stats', { @@ -339,8 +340,9 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { type LanguageDetectionPerfClassification = { owner: 'TylerLeonhardt'; - timeSpent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - detection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + comment: 'Helps understand how effective language detection and how long it takes to run'; + timeSpent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The time it took to run language detection' }; + detection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language that was detected' }; }; this._telemetryService.publicLog2<ILanguageDetectionPerf, LanguageDetectionPerfClassification>(LanguageDetectionStatsId, { diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index fa8078055ce..fa24a64d7af 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -45,6 +45,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { isArray, isObject } from 'vs/base/common/types'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; const emptyEditableSettingsContent = '{\n}'; @@ -67,6 +68,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @ITextModelService private readonly textModelResolverService: ITextModelService, @IKeybindingService keybindingService: IKeybindingService, @IModelService private readonly modelService: IModelService, @@ -166,7 +168,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.createDefaultSettingsEditorModel(uri); } - if (this.userSettingsResource.toString() === uri.toString()) { + if (this.userSettingsResource.toString() === uri.toString() || this.userDataProfilesService.defaultProfile.settingsResource.toString() === uri.toString()) { return this.createEditableSettingsEditorModel(ConfigurationTarget.USER_LOCAL, uri); } @@ -248,6 +250,14 @@ export class PreferencesService extends Disposable implements IPreferencesServic return this.editorGroupService.activeGroup.activeEditorPane!; } + openApplicationSettings(options: IOpenSettingsOptions = {}): Promise<IEditorPane | undefined> { + options = { + ...options, + target: ConfigurationTarget.USER_LOCAL, + }; + return this.open(this.userDataProfilesService.defaultProfile.settingsResource, options); + } + openUserSettings(options: IOpenSettingsOptions = {}): Promise<IEditorPane | undefined> { options = { ...options, @@ -450,6 +460,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic public async getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): Promise<URI | null> { switch (configurationTarget) { + case ConfigurationTarget.APPLICATION: + return this.userDataProfilesService.defaultProfile.settingsResource; case ConfigurationTarget.USER: case ConfigurationTarget.USER_LOCAL: return this.userSettingsResource; diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 06b43562999..c82d622e95f 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -233,6 +233,7 @@ export interface IPreferencesService { openRawDefaultSettings(): Promise<IEditorPane | undefined>; openSettings(options?: IOpenSettingsOptions): Promise<IEditorPane | undefined>; + openApplicationSettings(options?: IOpenSettingsOptions): Promise<IEditorPane | undefined>; openUserSettings(options?: IOpenSettingsOptions): Promise<IEditorPane | undefined>; openRemoteSettings(options?: IOpenSettingsOptions): Promise<IEditorPane | undefined>; openWorkspaceSettings(options?: IOpenSettingsOptions): Promise<IEditorPane | undefined>; diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 8172f41fa24..c39c0a727da 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -93,7 +93,7 @@ export class ProgressService extends Disposable implements IProgressService { } } - private readonly windowProgressStack: [IProgressOptions, Progress<IProgressStep>][] = []; + private readonly windowProgressStack: [IProgressWindowOptions, Progress<IProgressStep>][] = []; private windowProgressStatusEntry: IStatusbarEntryAccessor | undefined = undefined; private withWindowProgress<R = unknown>(options: IProgressWindowOptions, callback: (progress: IProgress<{ message?: string }>) => Promise<R>): Promise<R> { @@ -158,7 +158,7 @@ export class ProgressService extends Disposable implements IProgressService { const statusEntryProperties: IStatusbarEntry = { name: localize('status.progress', "Progress Message"), text, - showProgress: true, + showProgress: options.type || true, ariaLabel: text, tooltip: title, command: progressCommand diff --git a/src/vs/workbench/services/statusbar/browser/statusbar.ts b/src/vs/workbench/services/statusbar/browser/statusbar.ts index a4046d9c83d..6b55bb6860d 100644 --- a/src/vs/workbench/services/statusbar/browser/statusbar.ts +++ b/src/vs/workbench/services/statusbar/browser/statusbar.ts @@ -182,9 +182,10 @@ export interface IStatusbarEntry { readonly showBeak?: boolean; /** - * Will enable a spinning icon in front of the text to indicate progress. + * Will enable a spinning icon in front of the text to indicate progress. When `true` is + * specified, `syncing` will be used. */ - readonly showProgress?: boolean; + readonly showProgress?: boolean | 'syncing' | 'loading'; } export interface IStatusbarEntryAccessor extends IDisposable { diff --git a/src/vs/workbench/services/storage/test/browser/storageService.test.ts b/src/vs/workbench/services/storage/test/browser/storageService.test.ts index 8491d3f9301..cbe4837bc8a 100644 --- a/src/vs/workbench/services/storage/test/browser/storageService.test.ts +++ b/src/vs/workbench/services/storage/test/browser/storageService.test.ts @@ -89,7 +89,7 @@ flakySuite('StorageService (browser specific)', () => { disposables.clear(); }); - test('clear', () => { + test.skip('clear', () => { // slow test and also only ever being used as a developer action return runWithFakedTimers({ useFakeTimers: true }, async () => { storageService.store('bar', 'foo', StorageScope.APPLICATION, StorageTarget.MACHINE); storageService.store('bar', 3, StorageScope.APPLICATION, StorageTarget.USER); diff --git a/src/vs/workbench/services/textfile/common/textEditorService.ts b/src/vs/workbench/services/textfile/common/textEditorService.ts index a4a51339630..886acda45d2 100644 --- a/src/vs/workbench/services/textfile/common/textEditorService.ts +++ b/src/vs/workbench/services/textfile/common/textEditorService.ts @@ -75,9 +75,11 @@ export class TextEditorService extends Disposable implements ITextEditorService priority: RegisteredEditorPriority.builtin }, {}, - editor => ({ editor: this.createTextEditor(editor) }), - untitledEditor => ({ editor: this.createTextEditor(untitledEditor) }), - diffEditor => ({ editor: this.createTextEditor(diffEditor) }) + { + createEditorInput: editor => ({ editor: this.createTextEditor(editor) }), + createUntitledEditorInput: untitledEditor => ({ editor: this.createTextEditor(untitledEditor) }), + createDiffEditorInput: diffEditor => ({ editor: this.createTextEditor(diffEditor) }) + } )); } diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 3800c0fbd23..6745ad1bc49 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -581,11 +581,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (!this.themeExtensionsActivated.get(key)) { type ActivatePluginClassification = { owner: 'aeschli'; - id: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; - name: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; - isBuiltin: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - publisherDisplayName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - themeId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; + comment: 'An event is fired when an color theme extension is first used as it provides the currently shown color theme.'; + id: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The extension id.' }; + name: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The extension name.' }; + isBuiltin: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the extension is a built-in extension.' }; + publisherDisplayName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension publisher id.' }; + themeId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The id of the theme that triggered the first extension use.' }; }; type ActivatePluginEvent = { id: string; diff --git a/src/vs/workbench/services/update/browser/updateService.ts b/src/vs/workbench/services/update/browser/updateService.ts index 37814d5b856..fdfabe6f12b 100644 --- a/src/vs/workbench/services/update/browser/updateService.ts +++ b/src/vs/workbench/services/update/browser/updateService.ts @@ -90,6 +90,10 @@ export class BrowserUpdateService extends Disposable implements IUpdateService { async quitAndInstall(): Promise<void> { this.hostService.reload(); } + + async _applySpecificUpdate(packagePath: string): Promise<void> { + // noop + } } registerSingleton(IUpdateService, BrowserUpdateService); diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts index 3b1578dc366..5fab982a11c 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -32,7 +32,7 @@ export class UserDataProfileManagementService extends Disposable implements IUse private onDidChangeProfiles(e: DidChangeProfilesEvent): void { if (e.removed.some(profile => profile.id === this.userDataProfileService.currentProfile.id)) { - this.enterProfile(this.userDataProfilesService.defaultProfile, false, localize('reload message when removed', "The current profile has been removed. Please reload to switch back to default profile")); + this.enterProfile(this.userDataProfilesService.defaultProfile, false, localize('reload message when removed', "The current settings profile has been removed. Please reload to switch back to default settings profile")); return; } } @@ -43,15 +43,22 @@ export class UserDataProfileManagementService extends Disposable implements IUse return profile; } - async removeProfile(profile: IUserDataProfile): Promise<void> { + async renameProfile(profile: IUserDataProfile, name: string): Promise<void> { if (!this.userDataProfilesService.profiles.some(p => p.id === profile.id)) { - throw new Error(`Profile ${profile.name} does not exist`); + throw new Error(`Settings profile ${profile.name} does not exist`); } if (profile.isDefault) { - throw new Error(localize('cannotDeleteDefaultProfile', "Cannot delete the default profile")); + throw new Error(localize('cannotRenameDefaultProfile', "Cannot rename the default settings profile")); + } + await this.userDataProfilesService.updateProfile(profile, name); + } + + async removeProfile(profile: IUserDataProfile): Promise<void> { + if (!this.userDataProfilesService.profiles.some(p => p.id === profile.id)) { + throw new Error(`Settings profile ${profile.name} does not exist`); } - if (profile.id === this.userDataProfileService.currentProfile.id) { - throw new Error(localize('cannotDeleteCurrentProfile', "Cannot delete the current profile")); + if (profile.isDefault) { + throw new Error(localize('cannotDeleteDefaultProfile', "Cannot delete the default settings profile")); } await this.userDataProfilesService.removeProfile(profile); } @@ -83,7 +90,7 @@ export class UserDataProfileManagementService extends Disposable implements IUse if (this.environmentService.remoteAuthority) { const result = await this.dialogService.confirm({ type: 'info', - message: reloadMessage ?? localize('reload message', "Switching a profile requires reloading VS Code."), + message: reloadMessage ?? localize('reload message', "Switching a settings profile requires reloading VS Code."), primaryButton: localize('reload button', "&&Reload"), }); if (result.confirmed) { diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts index 6e09fd1d153..054bf392034 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -9,7 +9,7 @@ import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IUserDataProfile, PROFILES_ENABLEMENT_CONFIG, UseDefaultProfileFlags } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { ContextKeyDefinedExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyDefinedExpr, ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ProductQualityContext } from 'vs/platform/contextkey/common/contextkeys'; export interface DidChangeUserDataProfileEvent { @@ -22,6 +22,7 @@ export interface DidChangeUserDataProfileEvent { export const IUserDataProfileService = createDecorator<IUserDataProfileService>('IUserDataProfileService'); export interface IUserDataProfileService { readonly _serviceBrand: undefined; + readonly onDidUpdateCurrentProfile: Event<void>; readonly onDidChangeCurrentProfile: Event<DidChangeUserDataProfileEvent>; readonly currentProfile: IUserDataProfile; updateCurrentProfile(currentProfile: IUserDataProfile, preserveData: boolean): Promise<void>; @@ -33,6 +34,7 @@ export interface IUserDataProfileManagementService { createAndEnterProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, fromExisting?: boolean): Promise<IUserDataProfile>; removeProfile(profile: IUserDataProfile): Promise<void>; + renameProfile(profile: IUserDataProfile, name: string): Promise<void>; switchProfile(profile: IUserDataProfile): Promise<void>; } @@ -74,3 +76,5 @@ export const PROFILES_CATEGORY = PROFILES_TTILE.value; export const PROFILE_EXTENSION = 'code-profile'; export const PROFILE_FILTER = [{ name: localize('profile', "Settings Profile"), extensions: [PROFILE_EXTENSION] }]; export const PROFILES_ENABLEMENT_CONTEXT = ContextKeyExpr.and(ProductQualityContext.notEqualsTo('stable'), ContextKeyDefinedExpr.create(`config.${PROFILES_ENABLEMENT_CONFIG}`)); +export const CURRENT_PROFILE_CONTEXT = new RawContextKey<string>('currentSettingsProfile', ''); +export const HAS_PROFILES_CONTEXT = new RawContextKey<boolean>('hasSettingsProfiles', false); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts index e03387a0b29..8dc5549e0e9 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts @@ -16,6 +16,9 @@ export class UserDataProfileService extends Disposable implements IUserDataProfi private readonly _onDidChangeCurrentProfile = this._register(new Emitter<DidChangeUserDataProfileEvent>()); readonly onDidChangeCurrentProfile = this._onDidChangeCurrentProfile.event; + private readonly _onDidUpdateCurrentProfile = this._register(new Emitter<void>()); + readonly onDidUpdateCurrentProfile = this._onDidUpdateCurrentProfile.event; + private _currentProfile: IUserDataProfile; get currentProfile(): IUserDataProfile { return this._currentProfile; } @@ -25,13 +28,20 @@ export class UserDataProfileService extends Disposable implements IUserDataProfi ) { super(); this._currentProfile = currentProfile; - this._register(userDataProfilesService.onDidChangeProfiles(() => { + this._register(userDataProfilesService.onDidChangeProfiles(e => { /** * If the current profile is default profile, then reset it because, * In Desktop the extensions resource will be set/unset in the default profile when profiles are changed. */ if (this._currentProfile.isDefault) { this._currentProfile = userDataProfilesService.defaultProfile; + return; + } + + const updatedCurrentProfile = e.updated.find(p => this._currentProfile.id === p.id); + if (updatedCurrentProfile) { + this._currentProfile = updatedCurrentProfile; + this._onDidUpdateCurrentProfile.fire(); } })); } diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 16b1dff68d9..eefe8b7ffcd 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -15,7 +15,7 @@ import { IRemoteAuthorityResolverService, ResolverResult } from 'vs/platform/rem import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { isVirtualResource } from 'vs/platform/workspace/common/virtualWorkspace'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ISingleFolderWorkspaceIdentifier, isSavedWorkspace, isSingleFolderWorkspaceIdentifier, IWorkspace, IWorkspaceContextService, IWorkspaceFolder, toWorkspaceIdentifier, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { ISingleFolderWorkspaceIdentifier, isSavedWorkspace, isSingleFolderWorkspaceIdentifier, isTemporaryWorkspace, IWorkspace, IWorkspaceContextService, IWorkspaceFolder, toWorkspaceIdentifier, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustInfo, IWorkspaceTrustUriInfo, IWorkspaceTrustRequestService, IWorkspaceTrustTransitionParticipant, WorkspaceTrustUriResponse, IWorkspaceTrustEnablementService } from 'vs/platform/workspace/common/workspaceTrust'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -132,7 +132,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork this._workspaceTrustInitializedPromiseResolve = resolve; }); - this._storedTrustState = new WorkspaceTrustMemento(isWeb && this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : this.storageService); + this._storedTrustState = new WorkspaceTrustMemento(isWeb && this.isEmptyWorkspace() ? undefined : this.storageService); this._trustTransitionManager = this._register(new WorkspaceTrustTransitionManager()); this._trustStateInfo = this.loadTrustInfo(); @@ -172,7 +172,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } // Empty workspace - save initial state to memento - if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + if (this.isEmptyWorkspace()) { this._workspaceTrustInitializedPromise.then(() => { if (this._storedTrustState.isEmptyWorkspaceTrusted === undefined) { this._storedTrustState.isEmptyWorkspaceTrusted = this.isWorkspaceTrusted(); @@ -307,7 +307,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } // Empty workspace - use memento, open ediors, or user setting - if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + if (this.isEmptyWorkspace()) { // Use memento if present if (this._storedTrustState.isEmptyWorkspaceTrusted !== undefined) { return this._storedTrustState.isEmptyWorkspaceTrusted; @@ -426,6 +426,19 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } } + private isEmptyWorkspace(): boolean { + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + return true; + } + + const workspace = this.workspaceService.getWorkspace(); + if (workspace) { + return isTemporaryWorkspace(this.workspaceService.getWorkspace()) && workspace.folders.length === 0; + } + + return false; + } + private isTrustedVirtualResource(uri: URI): boolean { return isVirtualResource(uri) && uri.scheme !== 'vscode-vfs'; } @@ -451,7 +464,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } // Empty workspace - save memento - if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + if (this.isEmptyWorkspace()) { this._storedTrustState.isEmptyWorkspaceTrusted = value; } } @@ -530,7 +543,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } // Empty workspace - if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + if (this.isEmptyWorkspace()) { return true; } @@ -577,7 +590,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork async setWorkspaceTrust(trusted: boolean): Promise<void> { // Empty workspace - if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + if (this.isEmptyWorkspace()) { await this.updateWorkspaceTrust(trusted); return; } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index ff291f79a9f..de310cbfb0d 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -2009,6 +2009,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens export class TestUserDataProfileService implements IUserDataProfileService { readonly _serviceBrand: undefined; + readonly onDidUpdateCurrentProfile = Event.None; readonly onDidChangeCurrentProfile = Event.None; readonly currentProfile = toUserDataProfile('test', URI.file('tests').with({ scheme: 'vscode-tests' })); async updateCurrentProfile(): Promise<void> { } diff --git a/src/vscode-dts/vscode.proposed.contribViewSize.d.ts b/src/vscode-dts/vscode.proposed.contribViewSize.d.ts new file mode 100644 index 00000000000..01ec28d95f4 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.contribViewSize.d.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// empty placeholder for view size + +// https://github.com/microsoft/vscode/issues/122283 @alexr00 + +/** + * View contributions can include a `size`, which can be a number. A number works similar to the css flex property. + * + * For example, if you have 3 views, with sizes 1, 1, and 2, the views of size 1 will together take up the same amount of space as the view of size 2. + * + * A number value will only be used as an initial size. After a user has changed the size of the view, the user's choice will be restored. +*/ + diff --git a/src/vscode-dts/vscode.proposed.contribWebviewContext.d.ts b/src/vscode-dts/vscode.proposed.contribWebviewContext.d.ts new file mode 100644 index 00000000000..52037dc65b0 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.contribWebviewContext.d.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// empty placeholder declaration for the `webview/context`-menu contribution point diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index ed88fd3c2b8..f8dfe1db136 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -164,11 +164,6 @@ export class Code { }); } - if (retries === 40) { - done = true; - reject(new Error('Smoke test exit call did not terminate process after 20s, giving up')); - } - try { process.kill(pid, 0); // throws an exception if the process doesn't exist anymore. await new Promise(resolve => setTimeout(resolve, 500)); @@ -176,6 +171,12 @@ export class Code { done = true; resolve(); } + + if (retries === 60) { + done = true; + this.logger.log('Smoke test exit call did not terminate process after 30s, giving up'); + resolve(); + } } })(); }), 'Code#exit()', this.logger); diff --git a/test/automation/src/terminal.ts b/test/automation/src/terminal.ts index 67f012bf483..d088f849520 100644 --- a/test/automation/src/terminal.ts +++ b/test/automation/src/terminal.ts @@ -93,7 +93,14 @@ export class Terminal { await this._waitForTerminal(expectedLocation === 'editor' || commandId === TerminalCommandId.CreateNewEditor ? 'editor' : 'panel'); break; case TerminalCommandId.KillAll: - await this.code.waitForElements(Selector.Xterm, true, e => e.length === 0); + // HACK: Attempt to kill all terminals to clean things up, this is known to be flaky + // but the reason why isn't known. This is typically called in the after each hook, + // Since it's not actually required that all terminals are killed just continue on + // after 2 seconds. + await Promise.race([ + this.code.waitForElements(Selector.Xterm, true, e => e.length === 0), + new Promise<void>(r => setTimeout(r, 2000)) + ]); break; } } diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 305d78b819a..8a985c791c9 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -150,6 +150,8 @@ async function runTestsInBrowser(testModules, browserType) { const failingModuleIds = []; const failingTests = []; emitter.on('fail', (test, err) => { + failingTests.push({ title: test.fullTitle, message: err.message }); + if (err.stack) { const regex = /(vs\/.*\.test)\.js/; for (const line of String(err.stack).split('\n')) { @@ -160,9 +162,6 @@ async function runTestsInBrowser(testModules, browserType) { } } } - - // We could not determine the module id - failingTests.push(test.fullTitle); }); try { @@ -176,11 +175,14 @@ async function runTestsInBrowser(testModules, browserType) { } await browser.close(); - if (failingModuleIds.length > 0) { - return `to DEBUG, open ${browserType.toUpperCase()} and navigate to ${target.href}?${failingModuleIds.map(module => `m=${module}`).join('&')}`; - } if (failingTests.length > 0) { - return `The followings tests are failing:\n - ${failingTests.join('\n - ')}`; + let res = `The followings tests are failing:\n - ${failingTests.map(({ title, message }) => `${title} (reason: ${message})`).join('\n - ')}`; + + if (failingModuleIds.length > 0) { + res += `\n\nTo DEBUG, open ${browserType.toUpperCase()} and navigate to ${target.href}?${failingModuleIds.map(module => `m=${module}`).join('&')}`; + } + + return `${res}\n`; } } diff --git a/yarn.lock b/yarn.lock index b49d8d2188d..b11b23e316f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -805,6 +805,13 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@koa/cors@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.3.0.tgz#b4c1c7ee303b7c968c8727f2a638a74675b50bb2" + integrity sha512-lzlkqLlL5Ond8jb6JLnVVDmD2OPym0r5kvZlMgAWiS9xle+Q5ulw1T358oW+RVguxUkANquZQz82i/STIRmsqQ== + dependencies: + vary "^1.1.2" + "@koa/router@^10.1.1": version "10.1.1" resolved "https://registry.yarnpkg.com/@koa/router/-/router-10.1.1.tgz#8e5a85c9b243e0bc776802c0de564561e57a5f78" @@ -1564,22 +1571,24 @@ command-line-args "^5.2.1" ts-morph "^13.0.3" -"@vscode/test-web@^0.0.22": - version "0.0.22" - resolved "https://registry.yarnpkg.com/@vscode/test-web/-/test-web-0.0.22.tgz#8767c80e7b16e73e78cf30da93d4dff5d4db148a" - integrity sha512-sm4WYidw26eFb1AReC8w5y4aOMdBb5ma5x3ukRJcun9iUB1ajz2nM18rxiYAVimUzrIMQHr9WqC8HYBYP8aNKQ== +"@vscode/test-web@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@vscode/test-web/-/test-web-0.0.29.tgz#00f19159cf3ae70fdfae4a909df66e407c8b5e56" + integrity sha512-QJwu3F6U+IT/X6UiRVQEe1tKSB1aRVDlWi5jAfnbXaAH8Gk4NrUFLxAB33mms82XQK4PuCTXAqNd/eC8v3ZQDA== dependencies: + "@koa/cors" "^3.3.0" "@koa/router" "^10.1.1" decompress "^4.2.1" decompress-targz "^4.1.1" + get-stream "6.0.1" http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" koa "^2.13.4" koa-morgan "^1.0.1" koa-mount "^4.0.0" koa-static "^5.0.0" - minimist "^1.2.5" - playwright "^1.18.1" + minimist "^1.2.6" + playwright "^1.23.1" vscode-uri "^3.0.3" "@vscode/vscode-languagedetection@1.0.21": @@ -2668,9 +2677,9 @@ buffer-fill@^1.0.0: integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== buffer-xor@^1.0.3: version "1.0.3" @@ -3227,27 +3236,17 @@ command-line-args@^5.2.1: lodash.camelcase "^4.3.0" typical "^4.0.0" -commander@*, commander@^2.11.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.0.tgz#ad2a23a1c3b036e392469b8012cec6b33b4c1322" - integrity sha512-7B1ilBwtYSbetCgTY1NJFg+gVpestg0fdA1MhC1Vs4ssyfSXnCAjFr+QcQM9/RedXC0EaUx1sG8Smgw2VfgKEg== +commander@*, commander@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== commander@2.11.x: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ== -commander@8.3.0, commander@^8.2.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - -commander@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== - -commander@^2.20.0, commander@^2.8.1: +commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -4808,7 +4807,7 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@2.0.1, extract-zip@^2.0.1: +extract-zip@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -5350,6 +5349,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-stream@6.0.1, get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-stream@^2.2.0: version "2.3.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" @@ -5372,11 +5376,6 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -6138,6 +6137,14 @@ https-proxy-agent@^2.2.3: agent-base "^4.3.0" debug "^3.1.0" +https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -6890,7 +6897,7 @@ jest-worker@^27.0.2: merge-stream "^2.0.0" supports-color "^8.0.0" -jpeg-js@0.4.3, jpeg-js@^0.4.2: +jpeg-js@0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b" integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q== @@ -7671,11 +7678,6 @@ mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.6: - version "2.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" - integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== - mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -8849,34 +8851,17 @@ playwright-core@1.21.0: yauzl "2.10.0" yazl "2.5.1" -playwright-core@=1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.18.1.tgz#a5cf3f212d10692382e2acd1f7bc8c9ff9bbb849" - integrity sha512-NALGl8R1GHzGLlhUApmpmfh6M1rrrPcDTygWvhTbprxwGB9qd/j9DRwyn4HTQcUB6o0/VOpo46fH9ez3+D/Rog== - dependencies: - commander "^8.2.0" - debug "^4.1.1" - extract-zip "^2.0.1" - https-proxy-agent "^5.0.0" - jpeg-js "^0.4.2" - mime "^2.4.6" - pngjs "^5.0.0" - progress "^2.0.3" - proper-lockfile "^4.1.1" - proxy-from-env "^1.1.0" - rimraf "^3.0.2" - socks-proxy-agent "^6.1.0" - stack-utils "^2.0.3" - ws "^7.4.6" - yauzl "^2.10.0" - yazl "^2.5.1" +playwright-core@1.23.4: + version "1.23.4" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.23.4.tgz#e8a45e549faf6bfad24a0e9998e451979514d41e" + integrity sha512-h5V2yw7d8xIwotjyNrkLF13nV9RiiZLHdXeHo+nVJIYGVlZ8U2qV0pMxNJKNTvfQVT0N8/A4CW6/4EW2cOcTiA== -playwright@^1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.18.1.tgz#45c2ca6ee25c44e336985de9b51955727b5f17cf" - integrity sha512-8EaX9EtbtAoMq5tnzIsoA3b/V86V/6Mq2skuOU4qEw+5OVxs1lwesDwmjy/RVU1Qfx5UuwSQzhp45wyH22oa+A== +playwright@^1.23.1: + version "1.23.4" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.23.4.tgz#a9641a8d523fafdc58a5a2b59efe3496dec49c8d" + integrity sha512-NUPOLMpd8WydmwZFllST/YZ7cImgDDDrvcaq7Gj2vAjNg0jYCndFJt6HHtbkOPSIlRo4BaQYlbFx6meq1r1FXQ== dependencies: - playwright-core "=1.18.1" + playwright-core "1.23.4" plist@^3.0.1: version "3.0.5" @@ -8917,11 +8902,6 @@ pngjs@^4.0.1: resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe" integrity sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg== -pngjs@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" - integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== - posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -9333,7 +9313,7 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -proper-lockfile@4.1.2, proper-lockfile@^4.1.1: +proper-lockfile@4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== @@ -10228,7 +10208,7 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -socks-proxy-agent@6.1.1, socks-proxy-agent@^6.1.0: +socks-proxy-agent@6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew== @@ -10293,9 +10273,9 @@ source-map-support@^0.3.2: source-map "0.1.32" source-map-support@~0.5.12, source-map-support@~0.5.19: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -10922,9 +10902,9 @@ terser-webpack-plugin@^5.1.3: terser "^5.7.0" terser@^4.1.2: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + version "4.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" + integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -11273,10 +11253,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.8.0-dev.20220711: - version "4.8.0-dev.20220711" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.0-dev.20220711.tgz#3d4f68161716cb6cb1ea42fd0c6cc4b842f150b1" - integrity sha512-Nz1HlAkzZJ/OYZxqDEdoNV9GMq61xUss3JjveQqtdTiwhouLMa6D69C5K+P/fZD/hfrkMf/iqaF7xqVtX5KvPg== +typescript@^4.8.0-dev.20220719: + version "4.8.0-dev.20220719" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.0-dev.20220719.tgz#5481fe69ef18473d0da5ed23512d5754a2f998ef" + integrity sha512-IAZp6IDszN9iZi7R5LOqR5j0Ffy737RVQF7IefH1hNtFE+HiTjfsEYtWD2M0X/2feOCESZEKaa+GmuOVFuFhUQ== typical@^4.0.0: version "4.0.0" @@ -12028,11 +12008,6 @@ ws@^7.2.0: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== -ws@^7.4.6: - version "7.5.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" - integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== - xml2js@^0.4.17: version "0.4.23" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" @@ -12081,35 +12056,35 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" -xterm-addon-search@0.10.0-beta.2: - version "0.10.0-beta.2" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.2.tgz#a937d1e9a70fde8eeb7d1df485039b2d5fc1d707" - integrity sha512-ybafAbX9V4sfkzmUsWmtfEYExG8jj73bTF9pEa/Lhd5q4bviW4LcFaw/n3lKHn/1tSgSVgzoD13u1ZaZR78SfQ== +xterm-addon-search@0.10.0-beta.3: + version "0.10.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.3.tgz#5194434d86105637c71f6f20139a9d0b5c1a956a" + integrity sha512-UeGm/ymnp7HUYJJtsP0D+bljOWbdk3MctcLJ+0jv8AmU6YlAzJFtouvYSQrD5SAMyht5CRsvjzFgqic9X02JYg== -xterm-addon-serialize@0.8.0-beta.2: - version "0.8.0-beta.2" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0-beta.2.tgz#f30656d4ff1570ac105bacffe443385666654598" - integrity sha512-IDaRxO1zwjF9fDJp6u27Lv8852kEZ0HlbB0wLZbcIGZxDuPDLfvw8s/BV7f6MFB+mZq19CjyHGH4oPzZkc0rLQ== +xterm-addon-serialize@0.8.0-beta.3: + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0-beta.3.tgz#47ade3fedacbb75bd26e63cfe0120586623e0e4f" + integrity sha512-gvfempZCYuAhLqN4O6fA2TuoavPjOxFKlh8hLcOzPackiLUhwKr1jQpDXcnq8VgqUiGgb+XNZpPEbI0Q7EhTgA== 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.13.0-beta.7: - version "0.13.0-beta.7" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.7.tgz#9b514fa9792ced755c8321ddd06529f9912db2a1" - integrity sha512-U3sKzkziZRwb13MyNp0eMwt5BWyM938epAv0ZOnI5Vjq06S2naS37GgO/FY+0eu5wuBKkZhT9lNDv7n8rMqd+w== +xterm-addon-webgl@0.13.0-beta.9: + version "0.13.0-beta.9" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.9.tgz#66a9ac142ae347d0548abbf4e66bb2f35f415adb" + integrity sha512-x1o1tpCqIsICvhcRsZs+BLcwUIdizYS2G4TIH0KBnUDiSN+oSqpVBQNG8qKg56xbK8WtpdbQ9dLB7JR2W5cX0g== -xterm-headless@4.20.0-beta.13: - version "4.20.0-beta.13" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.13.tgz#a7d9d8837e3f78e106006cc94cf63ec13a9fd991" - integrity sha512-y4YI+Ogv2R2I++tsyvx5Q7csAaN7mG2yMMMBb/u4dXnrFmSGYs/R8ZFkeHgAW4Ju4uI3Rizb+ZdwtN1uG043Rw== +xterm-headless@4.20.0-beta.20: + version "4.20.0-beta.20" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.20.tgz#da2d8131b02d6f1e37f47cc17e578f2c2980fbb6" + integrity sha512-JK4jUIiUH7TdzvMrpfDnbGxTuC4s7byjqnMHR8+gIpY8qCFjz0xcMFSbp+ZshxGwVyziI4jtJqTHZjFToT2/kw== -xterm@4.20.0-beta.13: - version "4.20.0-beta.13" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.13.tgz#7526e19310daa56a11c26eb81a2706593c1378ec" - integrity sha512-ovXGhvM/qDRNGlGO653WY1pxIZrnI4+ayVM7pCnxO/tRwUIym6LGS3NurYrhGYrXOjoLJZxQ4EjToAfHvAg2Gg== +xterm@4.20.0-beta.20: + version "4.20.0-beta.20" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.20.tgz#2979a31839f7b8ee3ffe4f063b40c02facdb0fed" + integrity sha512-ltDtTquH+33tXQPFSDqenbgz6LkvIob6l6Rac85L4aX5Ve7P3ubVLrq+lTFJGQn3iiwGqNmnE1t1EUuGhxsXcQ== y18n@^3.2.1: version "3.2.2" @@ -12272,7 +12247,7 @@ yauzl@^2.2.1: buffer-crc32 "~0.2.3" fd-slicer "~1.0.1" -yazl@2.5.1, yazl@^2.5.1: +yazl@2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== |