Merge branch 'main' into joh/cellUri

This commit is contained in:
Johannes Rieken 2022-07-19 15:47:57 +02:00 committed by GitHub
commit 44e25ba96e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
324 changed files with 5833 additions and 6400 deletions

View file

@ -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 #

View file

@ -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_QUALITY, 'oss') }}:
- script: |
set -e
./scripts/test.sh --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
- script: |
set -e
yarn test-node
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
- 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_QUALITY, 'oss') }}:
- script: |
./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-darwin-$(VSCODE_ARCH)" \
./scripts/test-web-integration.sh --browser webkit
displayName: Run integration tests (Browser, Webkit)
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
- ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}:
- script: |
set -e
APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH)
APP_NAME="`ls $APP_ROOT | head -n 1`"
INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \
VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \
./scripts/test-remote-integration.sh
displayName: Run integration tests (Remote)
timeoutInMinutes: 20
- script: |
set -e
VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \
./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
- ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}:
- script: |
set -e
APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH)
APP_NAME="`ls $APP_ROOT | head -n 1`"
yarn smoketest-no-compile --tracing --build "$APP_ROOT/$APP_NAME"
timeoutInMinutes: 20
displayName: Run smoke tests (Electron)
- script: |
set -e
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_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 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
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

View file

@ -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 }}

View file

@ -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

View file

@ -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 }}

View file

@ -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
@ -31,9 +22,11 @@ variables:
stages:
- stage: Compile
displayName: Compile & Hygiene
jobs:
- job: Compile
pool: vscode-1es-vscode-linux-18.04
displayName: Compile & Hygiene
pool: vscode-1es-vscode-linux-20.04
variables:
VSCODE_ARCH: x64
steps:
@ -41,74 +34,14 @@ stages:
parameters:
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
- stage: LinuxServerDependencies
- stage: Test
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 }}
- 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: Linux
dependsOn:
- Compile
- LinuxServerDependencies
pool: vscode-1es-vscode-linux-18.04
jobs:
- job: Linuxx64UnitTest
displayName: Unit Tests
container: vscode-bionic-x64
displayName: Linux (Unit Tests)
pool: vscode-1es-vscode-linux-20.04
# container: vscode-bionic-x64
timeoutInMinutes: 60
variables:
VSCODE_ARCH: x64
NPM_ARCH: x64
@ -122,8 +55,10 @@ stages:
VSCODE_RUN_INTEGRATION_TESTS: false
VSCODE_RUN_SMOKE_TESTS: false
- job: Linuxx64IntegrationTest
displayName: Integration Tests
container: vscode-bionic-x64
displayName: Linux (Integration Tests)
pool: vscode-1es-vscode-linux-20.04
# container: vscode-bionic-x64
timeoutInMinutes: 60
variables:
VSCODE_ARCH: x64
NPM_ARCH: x64
@ -137,8 +72,10 @@ stages:
VSCODE_RUN_INTEGRATION_TESTS: true
VSCODE_RUN_SMOKE_TESTS: false
- job: Linuxx64SmokeTest
displayName: Smoke Tests
container: vscode-bionic-x64
displayName: Linux (Smoke Tests)
pool: vscode-1es-vscode-linux-20.04
# container: vscode-bionic-x64
timeoutInMinutes: 60
variables:
VSCODE_ARCH: x64
NPM_ARCH: x64
@ -152,50 +89,94 @@ stages:
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: 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
# - 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

View file

@ -108,9 +108,11 @@ variables:
- name: VSCODE_BUILD_STAGE_WINDOWS
value: ${{ or(eq(parameters.VSCODE_BUILD_WIN32, true), eq(parameters.VSCODE_BUILD_WIN32_32BIT, true), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}
- name: VSCODE_BUILD_STAGE_LINUX
value: ${{ or(eq(parameters.VSCODE_BUILD_LINUX, true), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true), eq(parameters.VSCODE_BUILD_LINUX_ALPINE, true), eq(parameters.VSCODE_BUILD_LINUX_ALPINE_ARM64, true), eq(parameters.VSCODE_BUILD_WEB, true)) }}
value: ${{ or(eq(parameters.VSCODE_BUILD_LINUX, true), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true), eq(parameters.VSCODE_BUILD_LINUX_ALPINE, true), eq(parameters.VSCODE_BUILD_LINUX_ALPINE_ARM64, true)) }}
- name: VSCODE_BUILD_STAGE_MACOS
value: ${{ or(eq(parameters.VSCODE_BUILD_MACOS, true), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}
- name: VSCODE_BUILD_STAGE_WEB
value: ${{ eq(parameters.VSCODE_BUILD_WEB, true) }}
- name: VSCODE_CIBUILD
value: ${{ in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI') }}
- name: VSCODE_PUBLISH
@ -176,45 +178,45 @@ stages:
pool: vscode-1es-windows
jobs:
- ${{ if eq(variables['VSCODE_CIBUILD'], true) }}:
- job: WindowsUnitTests
displayName: Unit Tests
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: Integration Tests
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: Smoke Tests
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
- job: WindowsUnitTests
displayName: Unit Tests
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: Integration Tests
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: Smoke Tests
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
- ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32, true)) }}:
- job: Windows
@ -291,51 +293,51 @@ stages:
pool: vscode-1es-linux
jobs:
- ${{ if eq(variables['VSCODE_CIBUILD'], true) }}:
- 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: 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
- ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX, true)) }}:
- job: Linuxx64
@ -430,13 +432,6 @@ stages:
steps:
- template: linux/product-build-alpine.yml
- ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WEB, true)) }}:
- job: LinuxWeb
variables:
VSCODE_ARCH: x64
steps:
- template: web/product-build-web.yml
- ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_MACOS'], true)) }}:
- stage: macOS
dependsOn:
@ -447,62 +442,8 @@ stages:
BUILDSECMON_OPT_IN: true
jobs:
- ${{ if eq(variables['VSCODE_CIBUILD'], true) }}:
- 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
- ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS, true)) }}:
- job: macOS
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: false
- ${{ if eq(parameters.VSCODE_STEP_ON_IT, false) }}:
- job: macOSTest
- job: macOSUnitTest
displayName: Unit Tests
timeoutInMinutes: 90
variables:
VSCODE_ARCH: x64
@ -511,19 +452,73 @@ stages:
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }}
VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }}
VSCODE_RUN_SMOKE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }}
- ${{ if eq(variables['VSCODE_PUBLISH'], true) }}:
- job: macOSSign
dependsOn:
- macOS
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-sign.yml
- 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
- ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS, true)) }}:
- job: macOS
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: false
- ${{ if eq(parameters.VSCODE_STEP_ON_IT, false) }}:
- job: macOSTest
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: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }}
VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }}
VSCODE_RUN_SMOKE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }}
- ${{ if eq(variables['VSCODE_PUBLISH'], true) }}:
- job: macOSSign
dependsOn:
- macOS
timeoutInMinutes: 90
variables:
VSCODE_ARCH: x64
steps:
- template: darwin/product-build-darwin-sign.yml
- ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}:
- job: macOSARM64
@ -570,6 +565,19 @@ stages:
steps:
- template: darwin/product-build-darwin-sign.yml
- ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_WEB'], true)) }}:
- stage: Web
dependsOn:
- Compile
pool: vscode-1es-linux
jobs:
- ${{ if eq(parameters.VSCODE_BUILD_WEB, true) }}:
- job: Web
variables:
VSCODE_ARCH: x64
steps:
- template: web/product-build-web.yml
- ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), ne(variables['VSCODE_PUBLISH'], 'false')) }}:
- stage: Publish
dependsOn:

View file

@ -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: |

View file

@ -46,6 +46,7 @@ $stages = @(
if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' }
if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' }
if ($env:VSCODE_BUILD_STAGE_MACOS -eq 'True') { 'macOS' }
if ($env:VSCODE_BUILD_STAGE_WEB -eq 'True') { 'Web' }
)
do {

View file

@ -109,6 +109,7 @@ steps:
if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' }
if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' }
if ($env:VSCODE_BUILD_STAGE_MACOS -eq 'True') { 'macOS' }
if ($env:VSCODE_BUILD_STAGE_WEB -eq 'True') { 'Web' }
)
Write-Host "Stages to check: $stages"

View file

@ -20,6 +20,7 @@ function main() {
.pipe(merge({
fileName: 'combined.nls.metadata.json',
jsonSpace: '',
concatArrays: true,
edit: (parsedJson, file) => {
if (file.base === 'out-vscode-web-min') {
return { vscode: parsedJson };

View file

@ -33,6 +33,7 @@ function main(): Promise<void> {
.pipe(merge({
fileName: 'combined.nls.metadata.json',
jsonSpace: '',
concatArrays: true,
edit: (parsedJson, file) => {
if (file.base === 'out-vscode-web-min') {
return { vscode: parsedJson };

View file

@ -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_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
- ${{ 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
- powershell: |
. build/azure-pipelines/win32/exec.ps1
$ErrorActionPreference = "Stop"
exec { yarn test-node }
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
- 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_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
- ${{ 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
- 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
- ${{ 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
- 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_QUALITY, 'oss') }}:
- powershell: |
. build/azure-pipelines/win32/exec.ps1
$ErrorActionPreference = "Stop"
exec { yarn --cwd test/smoke compile }
displayName: Compile smoke tests
- ${{ 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
- powershell: |
. build/azure-pipelines/win32/exec.ps1
$ErrorActionPreference = "Stop"
exec { yarn smoketest-no-compile --tracing }
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 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

View file

@ -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 }}

View file

@ -294,6 +294,10 @@
"name": "vs/workbench/contrib/audioCues",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/deprecatedExtensionMigrator",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/offline",
"project": "vscode-workbench"

View file

@ -16,6 +16,7 @@
"scmActionButton",
"scmSelectedProvider",
"scmValidation",
"tabInputTextMerge",
"timeline"
],
"categories": [

View file

@ -221,10 +221,10 @@
"config.timeline.date.committed": "Use the committed date",
"config.timeline.date.authored": "Use the authored date",
"config.useCommitInputAsStashMessage": "Controls whether to use the message from the commit input box as the default stash message.",
"config.showActionButton": "Controls whether an action button can be shown in the Source Control view.",
"config.showActionButton.commit": "Show an action button to commit changes.",
"config.showActionButton.publish": "Show an action button to publish a local branch.",
"config.showActionButton.sync": "Show an action button to sync changes.",
"config.showActionButton": "Controls whether an action button is shown in the Source Control view.",
"config.showActionButton.commit": "Show an action button to commit changes when the local branch has modified files ready to be committed.",
"config.showActionButton.publish": "Show an action button to publish the local branch when it does not have a tracking remote branch.",
"config.showActionButton.sync": "Show an action button to synchronize changes when the local branch is either ahead or behind the remote branch.",
"config.statusLimit": "Controls how to limit the number of changes that can be parsed from Git status command. Can be set to 0 for no limit.",
"config.experimental.installGuide": "Experimental improvements for the git setup flow.",
"config.repositoryScanIgnoredFolders": "List of folders that are ignored while scanning for Git repositories when `#git.autoRepositoryDetection#` is set to `true` or `subFolders`.",

View file

@ -50,6 +50,8 @@ export class ActionButtonCommand {
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
this.disposables.push(postCommitCommandsProviderRegistry.onDidChangePostCommitCommandsProviders(() => this._onDidChange.fire()));
const root = Uri.file(repository.root);
this.disposables.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git.enableSmartCommit', root) ||
@ -189,10 +191,10 @@ export class ActionButtonCommand {
return {
command: {
command: 'git.publish',
title: localize('scm publish branch action button title', "{0} Publish Branch", '$(cloud-upload)'),
title: localize({ key: 'scm publish branch action button title', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "{0} Publish Branch", '$(cloud-upload)'),
tooltip: this.state.isSyncInProgress ?
localize('scm button publish branch running', "Publishing Branch...") :
localize('scm button publish branch', "Publish Branch"),
localize({ key: 'scm button publish branch running', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "Publishing Branch...") :
localize({ key: 'scm button publish branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "Publish Branch"),
arguments: [this.repository.sourceControl],
},
enabled: !this.state.isSyncInProgress
@ -202,19 +204,18 @@ export class ActionButtonCommand {
private getSyncChangesActionButton(): SourceControlActionButton | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
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, commit/merge is in progress, or the button is disabled
if (!this.state.HEAD?.upstream || 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 is in progress, or the button is disabled
if (!this.state.HEAD?.upstream || !branchIsAheadOrBehind || this.state.isCommitInProgress || this.state.isMergeInProgress || !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)` : '';
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(sync)';
const rebaseWhenSync = config.get<string>('rebaseWhenSync');
return {
command: {
command: rebaseWhenSync ? 'git.syncRebase' : 'git.sync',
command: 'git.sync',
title: `${icon}${behind}${ahead}`,
tooltip: this.state.isSyncInProgress ?
localize('syncing changes', "Synchronizing Changes...")

View file

@ -5,7 +5,7 @@
import * as os from 'os';
import * as path from 'path';
import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText } from 'vscode';
import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge } from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import * as nls from 'vscode-nls';
import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator';
@ -53,7 +53,7 @@ class CheckoutTagItem extends CheckoutItem {
class CheckoutRemoteHeadItem extends CheckoutItem {
override get label(): string { return `$(git-branch) ${this.ref.name || this.shortCommit}`; }
override get label(): string { return `$(cloud) ${this.ref.name || this.shortCommit}`; }
override get description(): string {
return localize('remote branch at', "Remote branch at {0}", this.shortCommit);
}
@ -418,6 +418,7 @@ export class CommandCenter {
return;
}
const isRebasing = Boolean(repo.rebaseCommit);
type InputData = { uri: Uri; title?: string; detail?: string; description?: string };
const mergeUris = toMergeUris(uri);
@ -425,14 +426,17 @@ export class CommandCenter {
const theirs: InputData = { uri: mergeUris.theirs, title: localize('Theirs', 'Theirs') };
try {
const [head, mergeHead] = await Promise.all([repo.getCommit('HEAD'), repo.getCommit('MERGE_HEAD')]);
const [head, rebaseOrMergeHead] = await Promise.all([
repo.getCommit('HEAD'),
isRebasing ? repo.getCommit('REBASE_HEAD') : repo.getCommit('MERGE_HEAD')
]);
// ours (current branch and commit)
ours.detail = head.refNames.map(s => s.replace(/^HEAD ->/, '')).join(', ');
ours.description = head.hash.substring(0, 7);
// theirs
theirs.detail = mergeHead.refNames.join(', ');
theirs.description = mergeHead.hash.substring(0, 7);
theirs.detail = rebaseOrMergeHead.refNames.join(', ');
theirs.description = rebaseOrMergeHead.hash.substring(0, 7);
} catch (error) {
// not so bad, can continue with just uris
@ -442,8 +446,8 @@ export class CommandCenter {
const options = {
base: mergeUris.base,
input1: theirs,
input2: ours,
input1: isRebasing ? ours : theirs,
input2: isRebasing ? theirs : ours,
output: uri
};
@ -1099,21 +1103,26 @@ export class CommandCenter {
return;
}
const { activeTab } = window.tabGroups.activeTabGroup;
if (!activeTab) {
return;
}
// make sure to save the merged document
const doc = workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString());
if (!doc) {
console.log(`FAILED to accept merge because uri ${uri.toString()} doesn't match a document`);
return;
}
if (doc.isDirty) {
await doc.save();
}
await doc.save();
// TODO@jrieken there isn't a `TabInputTextMerge` instance yet, till now the merge editor
// uses the `TabInputText` for the out-resource and we use that to identify and CLOSE the tab
// see https://github.com/microsoft/vscode/issues/153213
const { activeTab } = window.tabGroups.activeTabGroup;
// find the merge editor tabs for the resource in question and close them all
let didCloseTab = false;
if (activeTab && activeTab?.input instanceof TabInputText && activeTab.input.uri.toString() === uri.toString()) {
didCloseTab = await window.tabGroups.close(activeTab, true);
const mergeEditorTabs = window.tabGroups.all.map(group => group.tabs.filter(tab => tab.input instanceof TabInputTextMerge && tab.input.result.toString() === uri.toString())).flat();
if (mergeEditorTabs.includes(activeTab)) {
didCloseTab = await window.tabGroups.close(mergeEditorTabs, true);
}
// Only stage if the merge editor has been successfully closed. That means all conflicts have been
@ -1443,7 +1452,7 @@ export class CommandCenter {
private async smartCommit(
repository: Repository,
getCommitMessage: () => Promise<string | undefined>,
opts?: CommitOptions
opts: CommitOptions
): Promise<boolean> {
const config = workspace.getConfiguration('git', Uri.file(repository.root));
let promptToSaveFilesBeforeCommit = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeCommit');
@ -1489,14 +1498,8 @@ export class CommandCenter {
}
}
if (!opts) {
opts = { all: noStagedChanges };
} else if (!opts.all && noStagedChanges && !opts.empty) {
opts = { ...opts, all: true };
}
// no changes, and the user has not configured to commit all in this case
if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.empty) {
if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.empty && !opts.all) {
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
if (!suggestSmartCommit) {
@ -1520,6 +1523,12 @@ export class CommandCenter {
}
}
if (opts.all === undefined) {
opts = { all: noStagedChanges };
} else if (!opts.all && noStagedChanges && !opts.empty) {
opts = { ...opts, all: true };
}
// enable signing of commits if configured
opts.signCommit = enableCommitSigning;
@ -1633,7 +1642,7 @@ export class CommandCenter {
return true;
}
private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
private async commitWithAnyInput(repository: Repository, opts: CommitOptions): Promise<void> {
const message = repository.inputBox.value;
const root = Uri.file(repository.root);
const config = workspace.getConfiguration('git', root);
@ -2566,17 +2575,16 @@ export class CommandCenter {
}
}
if (rebase) {
await repository.syncRebase(HEAD);
} else {
await repository.sync(HEAD);
}
await repository.sync(HEAD, rebase);
}
@command('git.sync', { repository: true })
async sync(repository: Repository): Promise<void> {
const config = workspace.getConfiguration('git', Uri.file(repository.root));
const rebase = config.get<boolean>('rebaseWhenSync', false) === true;
try {
await this._sync(repository, false);
await this._sync(repository, rebase);
} catch (err) {
if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
return;
@ -2589,13 +2597,16 @@ export class CommandCenter {
@command('git._syncAll')
async syncAll(): Promise<void> {
await Promise.all(this.model.repositories.map(async repository => {
const config = workspace.getConfiguration('git', Uri.file(repository.root));
const rebase = config.get<boolean>('rebaseWhenSync', false) === true;
const HEAD = repository.HEAD;
if (!HEAD || !HEAD.upstream) {
return;
}
await repository.sync(HEAD);
await repository.sync(HEAD, rebase);
}));
}

View file

@ -108,6 +108,9 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
private postCommitCommandsProviders = new Set<PostCommitCommandsProvider>();
private _onDidChangePostCommitCommandsProviders = new EventEmitter<void>();
readonly onDidChangePostCommitCommandsProviders = this._onDidChangePostCommitCommandsProviders.event;
private showRepoOnHomeDriveRootWarning = true;
private pushErrorHandlers = new Set<PushErrorHandler>();
@ -591,8 +594,12 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable {
this.postCommitCommandsProviders.add(provider);
this._onDidChangePostCommitCommandsProviders.fire();
return toDisposable(() => this.postCommitCommandsProviders.delete(provider));
return toDisposable(() => {
this.postCommitCommandsProviders.delete(provider);
this._onDidChangePostCommitCommandsProviders.fire();
});
}
getPostCommitCommandsProviders(): PostCommitCommandsProvider[] {

View file

@ -4,10 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { Command, Disposable } from 'vscode';
import { Command, Disposable, Event } from 'vscode';
import { PostCommitCommandsProvider } from './api/git';
export interface IPostCommitCommandsProviderRegistry {
readonly onDidChangePostCommitCommandsProviders: Event<void>;
getPostCommitCommandsProviders(): PostCommitCommandsProvider[];
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
}

View file

@ -948,7 +948,6 @@ export class Repository implements Disposable {
|| e.affectsConfiguration('git.untrackedChanges', root)
|| e.affectsConfiguration('git.ignoreSubmodules', root)
|| e.affectsConfiguration('git.openDiffOnClick', root)
|| e.affectsConfiguration('git.rebaseWhenSync', root)
|| e.affectsConfiguration('git.showActionButton', root)
)(this.updateModelState, this, this.disposables);
@ -1510,13 +1509,8 @@ export class Repository implements Disposable {
}
@throttle
sync(head: Branch): Promise<void> {
return this._sync(head, false);
}
@throttle
async syncRebase(head: Branch): Promise<void> {
return this._sync(head, true);
sync(head: Branch, rebase: boolean): Promise<void> {
return this._sync(head, rebase);
}
private async _sync(head: Branch, rebase: boolean): Promise<void> {

View file

@ -145,10 +145,7 @@ class SyncStatusBar {
text += this.repository.syncLabel;
}
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const rebaseWhenSync = config.get<string>('rebaseWhenSync');
command = rebaseWhenSync ? 'git.syncRebase' : 'git.sync';
command = 'git.sync';
tooltip = this.repository.syncTooltip;
} else {
icon = '$(cloud-upload)';

View file

@ -14,7 +14,7 @@
"../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts",
"../../src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts",
"../../src/vscode-dts/vscode.proposed.scmValidation.d.ts",
"../../src/vscode-dts/vscode.proposed.tabs.d.ts",
"../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts",
"../../src/vscode-dts/vscode.proposed.timeline.d.ts",
"../types/lib.textEncoder.d.ts"
]

View file

@ -26,7 +26,8 @@
}
},
"enabledApiProposals": [
"contribShareMenu"
"contribShareMenu",
"contribEditSessions"
],
"contributes": {
"commands": [
@ -37,10 +38,20 @@
{
"command": "github.copyVscodeDevLink",
"title": "Copy vscode.dev Link"
},
{
"command": "github.copyVscodeDevLinkFile",
"title": "Copy vscode.dev Link"
},
{
"command": "github.copyVscodeDevLinkFile",
"title": "Copy vscode.dev Link"
},
{
"command": "github.openOnVscodeDev",
"title": "Open on vscode.dev"
}
],
"continueEditSession": [
{
"command": "github.openOnVscodeDev",
"when": "github.hasGitHubRepo"
}
],
"menus": {
@ -56,6 +67,10 @@
{
"command": "github.copyVscodeDevLinkFile",
"when": "false"
},
{
"command": "github.openOnVscodeDev",
"when": "false"
}
],
"file/share": [

View file

@ -20,6 +20,16 @@ async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean) {
}
}
async function openVscodeDevLink(gitAPI: GitAPI): Promise<vscode.Uri | undefined> {
try {
const permalink = getPermalink(gitAPI, true, 'https://vscode.dev/github');
return permalink ? vscode.Uri.parse(permalink) : undefined;
} catch (err) {
vscode.window.showErrorMessage(err.message);
return undefined;
}
}
export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
const disposables = new DisposableStore();
@ -39,5 +49,9 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
return copyVscodeDevLink(gitAPI, false);
}));
disposables.add(vscode.commands.registerCommand('github.openOnVscodeDev', async () => {
return openVscodeDevLink(gitAPI);
}));
return disposables;
}

View file

@ -37,7 +37,8 @@
".editorconfig"
],
"filenames": [
"gitconfig"
"gitconfig",
".env"
],
"filenamePatterns": [
"**/.config/git/config",

View file

@ -16,7 +16,6 @@
"Programming Languages"
],
"enabledApiProposals": [
"textEditorDrop",
"documentPaste"
],
"activationEvents": [
@ -563,7 +562,8 @@
"@types/picomatch": "^2.3.0",
"@types/vscode-notebook-renderer": "^1.60.0",
"@types/vscode-webview": "^1.57.0",
"lodash.throttle": "^4.1.1"
"lodash.throttle": "^4.1.1",
"vscode-languageserver-types": "^3.17.2"
},
"repository": {
"type": "git",

View file

@ -28,7 +28,7 @@
"configuration.markdown.links.openLocation.currentGroup": "Open links in the active editor group.",
"configuration.markdown.links.openLocation.beside": "Open links beside the active editor.",
"configuration.markdown.suggest.paths.enabled.description": "Enable/disable path suggestions for markdown links",
"configuration.markdown.editor.drop.enabled": "Enable/disable dropping into the markdown editor to insert shift. Requires enabling `#workbench.experimental.editor.dropIntoEditor.enabled#`.",
"configuration.markdown.editor.drop.enabled": "Enable/disable dropping into the markdown editor to insert shift. Requires enabling `#workbench.editor.dropIntoEditor.enabled#`.",
"configuration.markdown.editor.pasteLinks.enabled": "Enable/disable pasting files into a Markdown editor inserts Markdown links. Requires enabling `#editor.experimental.pasteActions.enabled#`.",
"configuration.markdown.experimental.validate.enabled.description": "Enable/disable all error reporting in Markdown files.",
"configuration.markdown.experimental.validate.referenceLinks.enabled.description": "Validate reference links in Markdown files, e.g. `[link][ref]`. Requires enabling `#markdown.experimental.validate.enabled#`.",

View file

@ -6,7 +6,7 @@
"name": "Attach",
"type": "node",
"request": "attach",
"port": 7675,
"port": 7692,
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/out/**/*.js"]
}

View file

@ -10,17 +10,16 @@
"main": "./out/node/main",
"browser": "./dist/browser/main",
"dependencies": {
"vscode-languageserver": "^8.0.2-next.4",
"vscode-uri": "^3.0.3",
"vscode-languageserver": "^8.0.2-next.5`",
"vscode-languageserver-textdocument": "^1.0.5",
"vscode-languageserver-types": "^3.17.1",
"vscode-markdown-languageservice": "microsoft/vscode-markdown-languageservice"
"vscode-markdown-languageservice": "^0.0.0-alpha.8",
"vscode-uri": "^3.0.3"
},
"devDependencies": {
"@types/node": "16.x"
},
"scripts": {
"postinstall": "cd node_modules/vscode-markdown-languageservice && yarn run compile-ext",
"compile": "gulp compile-extension:markdown-language-features-server",
"watch": "gulp watch-extension:markdown-language-features-server"
}

View file

@ -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.
*--------------------------------------------------------------------------------------------*/
export interface LsConfiguration {
/**
* List of file extensions should be considered as markdown.
*
* These should not include the leading `.`.
*/
readonly markdownFileExtensions: readonly string[];
}
const defaultConfig: LsConfiguration = {
markdownFileExtensions: ['md'],
};
export function getLsConfiguration(overrides: Partial<LsConfiguration>): LsConfiguration {
return {
...defaultConfig,
...overrides,
};
}

View file

@ -5,11 +5,14 @@
import { RequestType } from 'vscode-languageserver';
import * as md from 'vscode-markdown-languageservice';
import * as lsp from 'vscode-languageserver-types';
declare const TextDecoder: any;
// 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');
// To server
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');

View file

@ -3,48 +3,92 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Connection, InitializeParams, InitializeResult, TextDocuments } from 'vscode-languageserver';
import { CancellationToken, Connection, InitializeParams, InitializeResult, NotebookDocuments, TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as lsp from 'vscode-languageserver-types';
import * as md from 'vscode-markdown-languageservice';
import { URI } from 'vscode-uri';
import { getLsConfiguration } from './config';
import { LogFunctionLogger } from './logging';
import { parseRequestType } from './protocol';
import * as protocol from './protocol';
import { VsCodeClientWorkspace } from './workspace';
declare const TextDecoder: any;
export function startServer(connection: Connection) {
export async function startServer(connection: Connection) {
const documents = new TextDocuments(TextDocument);
documents.listen(connection);
const notebooks = new NotebookDocuments(documents);
connection.onInitialize((_params: InitializeParams): InitializeResult => {
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() });
}
};
const config = getLsConfiguration({
markdownFileExtensions: params.initializationOptions.markdownFileExtensions,
});
const workspace = new VsCodeClientWorkspace(connection, config, documents, notebooks);
const logger = new LogFunctionLogger(connection.console.log.bind(connection.console));
provider = md.createLanguageService({
workspace,
parser,
logger,
markdownFileExtensions: config.markdownFileExtensions,
});
workspace.workspaceFolders = (params.workspaceFolders ?? []).map(x => URI.parse(x.uri));
return {
capabilities: {
completionProvider: { triggerCharacters: ['.', '/', '#'] },
definitionProvider: true,
documentLinkProvider: { resolveProvider: true },
documentSymbolProvider: true,
foldingRangeProvider: true,
renameProvider: { prepareProvider: true, },
selectionRangeProvider: true,
workspaceSymbolProvider: true,
workspace: {
workspaceFolders: {
supported: true,
changeNotifications: true,
},
}
}
};
});
const parser = new class implements md.IMdParser {
slugifier = md.githubSlugifier;
async tokenize(document: md.ITextDocument): Promise<md.Token[]> {
return await connection.sendRequest(parseRequestType, { uri: document.uri.toString() });
let provider: md.IMdLanguageService | undefined;
connection.onDocumentLinks(async (params, token): Promise<lsp.DocumentLink[]> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.getDocumentLinks(document, token);
}
} catch (e) {
console.error(e.stack);
}
};
return [];
});
const workspace = new VsCodeClientWorkspace(connection, documents);
const logger = new LogFunctionLogger(connection.console.log.bind(connection.console));
const provider = md.createLanguageService({ workspace, parser, logger });
connection.onDocumentLinkResolve(async (link, token): Promise<lsp.DocumentLink | undefined> => {
try {
return await provider!.resolveDocumentLink(link, token);
} catch (e) {
console.error(e.stack);
}
return undefined;
});
connection.onDocumentSymbol(async (params, token): Promise<lsp.DocumentSymbol[]> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider.provideDocumentSymbols(document, token);
return await provider!.getDocumentSymbols(document, token);
}
} catch (e) {
console.error(e.stack);
@ -56,7 +100,7 @@ export function startServer(connection: Connection) {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider.provideFoldingRanges(document, token);
return await provider!.getFoldingRanges(document, token);
}
} catch (e) {
console.error(e.stack);
@ -68,7 +112,7 @@ export function startServer(connection: Connection) {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider.provideSelectionRanges(document, params.positions, token);
return await provider!.getSelectionRanges(document, params.positions, token);
}
} catch (e) {
console.error(e.stack);
@ -78,13 +122,85 @@ export function startServer(connection: Connection) {
connection.onWorkspaceSymbol(async (params, token): Promise<lsp.WorkspaceSymbol[]> => {
try {
return await provider.provideWorkspaceSymbols(params.query, token);
return await provider!.getWorkspaceSymbols(params.query, token);
} catch (e) {
console.error(e.stack);
}
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);
if (document) {
return await provider!.getReferences(document, params.position, params.context, token);
}
} catch (e) {
console.error(e.stack);
}
return [];
});
connection.onDefinition(async (params, token): Promise<lsp.Definition | undefined> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.getDefinition(document, params.position, token);
}
} catch (e) {
console.error(e.stack);
}
return undefined;
});
connection.onPrepareRename(async (params, token) => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.prepareRename(document, params.position, token);
}
} catch (e) {
console.error(e.stack);
}
return undefined;
});
connection.onRenameRequest(async (params, token) => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
const edit = await provider!.getRenameEdit(document, params.position, params.newName, token);
console.log(JSON.stringify(edit));
return edit;
}
} catch (e) {
console.error(e.stack);
}
return undefined;
});
connection.onRequest(protocol.getReferencesToFileInWorkspace, (async (params: { uri: string }, token: CancellationToken) => {
try {
return await provider!.getFileReferences(URI.parse(params.uri), token);
} catch (e) {
console.error(e.stack);
}
return undefined;
}));
documents.listen(connection);
notebooks.listen(connection);
connection.listen();
}

View file

@ -4,24 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as URI from 'vscode-uri';
import { URI, Utils } from 'vscode-uri';
import { LsConfiguration } from '../config';
const markdownFileExtensions = Object.freeze<string[]>([
'.md',
'.mkd',
'.mdwn',
'.mdown',
'.markdown',
'.markdn',
'.mdtxt',
'.mdtext',
'.workbook',
]);
export function looksLikeMarkdownPath(resolvedHrefPath: URI.URI) {
return markdownFileExtensions.includes(URI.Utils.extname(URI.URI.from(resolvedHrefPath)).toLowerCase());
export function looksLikeMarkdownPath(config: LsConfiguration, resolvedHrefPath: URI) {
return config.markdownFileExtensions.includes(Utils.extname(URI.from(resolvedHrefPath)).toLowerCase().replace('.', ''));
}
export function isMarkdownDocument(document: TextDocument): boolean {
export function isMarkdownFile(document: TextDocument) {
return document.languageId === 'markdown';
}

View file

@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const Schemes = Object.freeze({
notebookCell: 'vscode-notebook-cell',
});

View file

@ -3,15 +3,18 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Connection, Emitter, FileChangeType, TextDocuments } from 'vscode-languageserver';
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 { URI } from 'vscode-uri';
import { LsConfiguration } from './config';
import * as protocol from './protocol';
import { coalesce } from './util/arrays';
import { isMarkdownDocument, looksLikeMarkdownPath } from './util/file';
import { isMarkdownFile, looksLikeMarkdownPath } from './util/file';
import { Limiter } from './util/limiter';
import { ResourceMap } from './util/resourceMap';
import { Schemes } from './util/schemes';
declare const TextDecoder: any;
@ -32,7 +35,9 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
constructor(
private readonly connection: Connection,
private readonly config: LsConfiguration,
private readonly documents: TextDocuments<TextDocument>,
private readonly notebooks: NotebookDocuments<TextDocument>,
) {
documents.onDidOpen(e => {
this._documentCache.delete(URI.parse(e.document.uri));
@ -57,14 +62,14 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
switch (change.type) {
case FileChangeType.Changed: {
this._documentCache.delete(resource);
const document = await this.getOrLoadMarkdownDocument(resource);
const document = await this.openMarkdownDocument(resource);
if (document) {
this._onDidChangeMarkdownDocument.fire(document);
}
break;
}
case FileChangeType.Created: {
const document = await this.getOrLoadMarkdownDocument(resource);
const document = await this.openMarkdownDocument(resource);
if (document) {
this._onDidCreateMarkdownDocument.fire(document);
}
@ -80,6 +85,22 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
});
}
public listen() {
this.connection.workspace.onDidChangeWorkspaceFolders(async () => {
this.workspaceFolders = (await this.connection.workspace.getWorkspaceFolders() ?? []).map(x => URI.parse(x.uri));
});
}
private _workspaceFolders: readonly URI[] = [];
get workspaceFolders(): readonly URI[] {
return this._workspaceFolders;
}
set workspaceFolders(value: readonly URI[]) {
this._workspaceFolders = value;
}
async getAllMarkdownDocuments(): Promise<Iterable<md.ITextDocument>> {
const maxConcurrent = 20;
@ -91,7 +112,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
const onDiskResults = await Promise.all(resources.map(strResource => {
return limiter.queue(async () => {
const resource = URI.parse(strResource);
const doc = await this.getOrLoadMarkdownDocument(resource);
const doc = await this.openMarkdownDocument(resource);
if (doc) {
foundFiles.set(resource);
}
@ -110,7 +131,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
return !!this.documents.get(resource.toString());
}
async getOrLoadMarkdownDocument(resource: URI): Promise<md.ITextDocument | undefined> {
async openMarkdownDocument(resource: URI): Promise<md.ITextDocument | undefined> {
const existing = this._documentCache.get(resource);
if (existing) {
return existing;
@ -122,7 +143,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
return matchingDocument;
}
if (!looksLikeMarkdownPath(resource)) {
if (!looksLikeMarkdownPath(this.config, resource)) {
return undefined;
}
@ -141,15 +162,31 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
}
}
async pathExists(_resource: URI): Promise<boolean> {
return false;
async stat(resource: URI): Promise<md.FileStat | undefined> {
if (this._documentCache.has(resource) || this.documents.get(resource.toString())) {
return { isDirectory: false };
}
return this.connection.sendRequest(protocol.statFileRequestType, { uri: resource.toString() });
}
async readDirectory(_resource: URI): Promise<[string, { isDir: boolean }][]> {
return [];
async readDirectory(resource: URI): Promise<[string, md.FileStat][]> {
return this.connection.sendRequest(protocol.readDirectoryRequestType, { uri: resource.toString() });
}
getContainingDocument(resource: URI): ContainingDocumentContext | undefined {
if (resource.scheme === Schemes.notebookCell) {
const nb = this.notebooks.findNotebookDocumentForCell(resource.toString());
if (nb) {
return {
uri: URI.parse(nb.uri),
children: nb.cells.map(cell => ({ uri: URI.parse(cell.document) })),
};
}
}
return undefined;
}
private isRelevantMarkdownDocument(doc: TextDocument) {
return isMarkdownDocument(doc) && URI.parse(doc.uri).scheme !== 'vscode-bulkeditpreview';
return isMarkdownFile(doc) && URI.parse(doc.uri).scheme !== 'vscode-bulkeditpreview';
}
}

View file

@ -35,17 +35,19 @@ vscode-languageserver-types@^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@^8.0.2-next.4:
vscode-languageserver@^8.0.2-next.5`:
version "8.0.2-next.5"
resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.0.2-next.5.tgz#39a2dd4c504fb88042375e7ac706a714bdaab4e5"
integrity sha512-2ZDb7O/4atS9mJKufPPz637z+51kCyZfgnobFW5eSrUdS3c0UB/nMS4Ng1EavYTX84GVaVMKCrmP0f2ceLmR0A==
dependencies:
vscode-languageserver-protocol "3.17.2-next.6"
vscode-markdown-languageservice@microsoft/vscode-markdown-languageservice:
version "0.0.0-alpha.2"
resolved "https://codeload.github.com/microsoft/vscode-markdown-languageservice/tar.gz/db497ada376aae9a335519dbfb406c6a1f873446"
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==
dependencies:
vscode-languageserver-textdocument "^1.0.5"
vscode-languageserver-types "^3.17.1"
vscode-uri "^3.0.3"

View file

@ -5,7 +5,7 @@
import Token = require('markdown-it/lib/token');
import * as vscode from 'vscode';
import { BaseLanguageClient, LanguageClientOptions, RequestType } from 'vscode-languageclient';
import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType, RequestType } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { IMdParser } from './markdownEngine';
import { markdownFileExtensions } from './util/file';
@ -14,9 +14,9 @@ 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;
@ -24,22 +24,36 @@ export type LanguageClientConstructor = (name: string, description: string, clie
export async function startClient(factory: LanguageClientConstructor, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
const documentSelector = ['markdown'];
const mdFileGlob = `**/*.{${markdownFileExtensions.join(',')}}`;
const clientOptions: LanguageClientOptions = {
documentSelector,
documentSelector: [{ language: 'markdown' }],
synchronize: {
configurationSection: ['markdown'],
fileEvents: vscode.workspace.createFileSystemWatcher(mdFileGlob),
},
initializationOptions: {}
initializationOptions: {
markdownFileExtensions,
}
};
const client = factory('markdown', localize('markdownServer.name', 'Markdown Language Server'), clientOptions);
client.registerProposedFeatures();
const notebookFeature = client.getFeature(NotebookDocumentSyncRegistrationType.method);
if (notebookFeature !== undefined) {
notebookFeature.register({
id: String(Date.now()),
registerOptions: {
notebookSelector: [{
notebook: '*',
cells: [{ language: 'markdown' }]
}]
}
});
}
client.onRequest(parseRequestType, async (e) => {
const uri = vscode.Uri.parse(e.uri);
const doc = await workspace.getOrLoadMarkdownDocument(uri);
@ -55,6 +69,22 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
return Array.from(await vscode.workspace.fs.readFile(uri));
});
client.onRequest(statFileRequestType, async (e): Promise<{ isDirectory: boolean } | undefined> => {
const uri = vscode.Uri.parse(e.uri);
try {
const stat = await vscode.workspace.fs.stat(uri);
return { isDirectory: stat.type === vscode.FileType.Directory };
} catch {
return undefined;
}
});
client.onRequest(readDirectoryRequestType, 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[]> => {
return (await vscode.workspace.findFiles(mdFileGlob, '**/node_modules/**')).map(x => x.toString());
});

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/browser';
import { BaseLanguageClient, LanguageClient, LanguageClientOptions } from 'vscode-languageclient/browser';
import { startClient } from './client';
import { activateShared } from './extension.shared';
import { VsCodeOutputLogger } from './logging';
@ -13,7 +13,7 @@ import { getMarkdownExtensionContributions } from './markdownExtensions';
import { githubSlugifier } from './slugify';
import { IMdWorkspace, VsCodeMdWorkspace } from './workspace';
export function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext) {
const contributions = getMarkdownExtensionContributions(context);
context.subscriptions.push(contributions);
@ -25,15 +25,15 @@ export function activate(context: vscode.ExtensionContext) {
const workspace = new VsCodeMdWorkspace();
context.subscriptions.push(workspace);
activateShared(context, workspace, engine, logger, contributions);
startServer(context, workspace, engine);
const client = await startServer(context, workspace, engine);
activateShared(context, client, workspace, engine, logger, contributions);
}
async function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<void> {
function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
const serverMain = vscode.Uri.joinPath(context.extensionUri, 'server/dist/browser/main.js');
const worker = new Worker(serverMain.toString());
await startClient((id: string, name: string, clientOptions: LanguageClientOptions) => {
return startClient((id: string, name: string, clientOptions: LanguageClientOptions) => {
return new LanguageClient(id, name, clientOptions, worker);
}, workspace, parser);
}

View file

@ -4,17 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { BaseLanguageClient } from 'vscode-languageclient';
import { CommandManager } from './commandManager';
import * as commands from './commands/index';
import { registerPasteSupport } from './languageFeatures/copyPaste';
import { registerDefinitionSupport } from './languageFeatures/definitions';
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
import { MdLinkProvider, registerDocumentLinkSupport } from './languageFeatures/documentLinks';
import { MdLinkProvider } from './languageFeatures/documentLinks';
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
import { registerPathCompletionSupport } from './languageFeatures/pathCompletions';
import { MdReferencesProvider, registerReferencesSupport } from './languageFeatures/references';
import { registerRenameSupport } from './languageFeatures/rename';
import { MdReferencesProvider } from './languageFeatures/references';
import { ILogger } from './logging';
import { IMdParser, MarkdownItEngine, MdParsingProvider } from './markdownEngine';
import { MarkdownContributionProvider } from './markdownExtensions';
@ -27,6 +25,7 @@ import { IMdWorkspace } from './workspace';
export function activateShared(
context: vscode.ExtensionContext,
client: BaseLanguageClient,
workspace: IMdWorkspace,
engine: MarkdownItEngine,
logger: ILogger,
@ -46,7 +45,7 @@ export function activateShared(
const previewManager = new MarkdownPreviewManager(contentProvider, workspace, logger, contributions, tocProvider);
context.subscriptions.push(previewManager);
context.subscriptions.push(registerMarkdownLanguageFeatures(parser, workspace, commandManager, tocProvider, logger));
context.subscriptions.push(registerMarkdownLanguageFeatures(client, parser, workspace, commandManager, tocProvider, logger));
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine, tocProvider));
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
@ -55,6 +54,7 @@ export function activateShared(
}
function registerMarkdownLanguageFeatures(
client: BaseLanguageClient,
parser: IMdParser,
workspace: IMdWorkspace,
commandManager: CommandManager,
@ -71,15 +71,10 @@ function registerMarkdownLanguageFeatures(
referencesProvider,
// Language features
registerDefinitionSupport(selector, referencesProvider),
registerDiagnosticSupport(selector, workspace, linkProvider, commandManager, referencesProvider, tocProvider, logger),
registerDocumentLinkSupport(selector, linkProvider),
registerDropIntoEditorSupport(selector),
registerFindFileReferenceSupport(commandManager, referencesProvider),
registerFindFileReferenceSupport(commandManager, client),
registerPasteSupport(selector),
registerPathCompletionSupport(selector, workspace, parser, linkProvider),
registerReferencesSupport(selector, referencesProvider),
registerRenameSupport(selector, workspace, referencesProvider, parser.slugifier),
);
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { LanguageClient, ServerOptions, TransportKind } from 'vscode-languageclient/node';
import { BaseLanguageClient, LanguageClient, ServerOptions, TransportKind } from 'vscode-languageclient/node';
import { startClient } from './client';
import { activateShared } from './extension.shared';
import { VsCodeOutputLogger } from './logging';
@ -13,7 +13,7 @@ import { getMarkdownExtensionContributions } from './markdownExtensions';
import { githubSlugifier } from './slugify';
import { IMdWorkspace, VsCodeMdWorkspace } from './workspace';
export function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext) {
const contributions = getMarkdownExtensionContributions(context);
context.subscriptions.push(contributions);
@ -25,11 +25,11 @@ export function activate(context: vscode.ExtensionContext) {
const workspace = new VsCodeMdWorkspace();
context.subscriptions.push(workspace);
activateShared(context, workspace, engine, logger, contributions);
startServer(context, workspace, engine);
const client = await startServer(context, workspace, engine);
activateShared(context, client, workspace, engine, logger, contributions);
}
async function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<void> {
function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
const clientMain = vscode.extensions.getExtension('vscode.markdown-language-features')?.packageJSON?.main || '';
const serverMain = `./server/${clientMain.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/main`;
@ -44,7 +44,7 @@ async function startServer(context: vscode.ExtensionContext, workspace: IMdWorks
run: { module: serverModule, transport: TransportKind.ipc },
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
};
await startClient((id, name, clientOptions) => {
return startClient((id, name, clientOptions) => {
return new LanguageClient(id, name, serverOptions, clientOptions);
}, workspace, parser);
}

View file

@ -1,27 +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 { ITextDocument } from '../types/textDocument';
import { MdReferencesProvider } from './references';
export class MdVsCodeDefinitionProvider implements vscode.DefinitionProvider {
constructor(
private readonly referencesProvider: MdReferencesProvider,
) { }
async provideDefinition(document: ITextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | undefined> {
const allRefs = await this.referencesProvider.getReferencesAtPosition(document, position, token);
return allRefs.find(ref => ref.kind === 'link' && ref.isDefinition)?.location;
}
}
export function registerDefinitionSupport(
selector: vscode.DocumentSelector,
referencesProvider: MdReferencesProvider,
): vscode.Disposable {
return vscode.languages.registerDefinitionProvider(selector, new MdVsCodeDefinitionProvider(referencesProvider));
}

View file

@ -4,21 +4,16 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as uri from 'vscode-uri';
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
import { ILogger } from '../logging';
import { IMdParser } from '../markdownEngine';
import { getLine, ITextDocument } from '../types/textDocument';
import { coalesce } from '../util/arrays';
import { noopToken } from '../util/cancellation';
import { Disposable } from '../util/dispose';
import { Schemes } from '../util/schemes';
import { MdDocumentInfoCache } from '../util/workspaceCache';
import { IMdWorkspace } from '../workspace';
const localize = nls.loadMessageBundle();
export interface ExternalHref {
readonly kind: 'external';
readonly uri: vscode.Uri;
@ -543,62 +538,3 @@ export class LinkDefinitionSet implements Iterable<[string, MdLinkDefinition]> {
return this._map.get(ref);
}
}
export class MdVsCodeLinkProvider implements vscode.DocumentLinkProvider {
constructor(
private readonly _linkProvider: MdLinkProvider,
) { }
public async provideDocumentLinks(
document: ITextDocument,
token: vscode.CancellationToken
): Promise<vscode.DocumentLink[]> {
const { links, definitions } = await this._linkProvider.getLinks(document);
if (token.isCancellationRequested) {
return [];
}
return coalesce(links.map(data => this.toValidDocumentLink(data, definitions)));
}
private toValidDocumentLink(link: MdLink, definitionSet: LinkDefinitionSet): vscode.DocumentLink | undefined {
switch (link.href.kind) {
case 'external': {
let target = link.href.uri;
// Normalize VS Code links to target currently running version
if (link.href.uri.scheme === Schemes.vscode || link.href.uri.scheme === Schemes['vscode-insiders']) {
target = target.with({ scheme: vscode.env.uriScheme });
}
return new vscode.DocumentLink(link.source.hrefRange, link.href.uri);
}
case 'internal': {
const uri = OpenDocumentLinkCommand.createCommandUri(link.source.resource, link.href.path, link.href.fragment);
const documentLink = new vscode.DocumentLink(link.source.hrefRange, uri);
documentLink.tooltip = localize('documentLink.tooltip', 'Follow link');
return documentLink;
}
case 'reference': {
// We only render reference links in the editor if they are actually defined.
// This matches how reference links are rendered by markdown-it.
const def = definitionSet.lookup(link.href.ref);
if (def) {
const documentLink = new vscode.DocumentLink(
link.source.hrefRange,
vscode.Uri.parse(`command:_markdown.moveCursorToPosition?${encodeURIComponent(JSON.stringify([def.source.hrefRange.start.line, def.source.hrefRange.start.character]))}`));
documentLink.tooltip = localize('documentLink.referenceTooltip', 'Go to link definition');
return documentLink;
} else {
return undefined;
}
}
}
}
}
export function registerDocumentLinkSupport(
selector: vscode.DocumentSelector,
linkProvider: MdLinkProvider,
): vscode.Disposable {
return vscode.languages.registerDocumentLinkProvider(selector, new MdVsCodeLinkProvider(linkProvider));
}

View file

@ -4,9 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { BaseLanguageClient } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commandManager';
import { MdReferencesProvider } from './references';
import { getReferencesToFileInWorkspace } from '../protocol';
const localize = nls.loadMessageBundle();
@ -16,7 +17,7 @@ export class FindFileReferencesCommand implements Command {
public readonly id = 'markdown.findAllFileReferences';
constructor(
private readonly referencesProvider: MdReferencesProvider,
private readonly client: BaseLanguageClient,
) { }
public async execute(resource?: vscode.Uri) {
@ -33,8 +34,9 @@ export class FindFileReferencesCommand implements Command {
location: vscode.ProgressLocation.Window,
title: localize('progress.title', "Finding file references")
}, async (_progress, token) => {
const references = await this.referencesProvider.getReferencesToFileInWorkspace(resource!, token);
const locations = references.map(ref => ref.location);
const locations = (await this.client.sendRequest(getReferencesToFileInWorkspace, { uri: resource!.toString() }, token)).map(loc => {
return new vscode.Location(vscode.Uri.parse(loc.uri), new vscode.Range(loc.range.start.line, loc.range.start.character, loc.range.end.line, loc.range.end.character));
});
const config = vscode.workspace.getConfiguration('references');
const existingSetting = config.inspect<string>('preferredLocation');
@ -51,7 +53,7 @@ export class FindFileReferencesCommand implements Command {
export function registerFindFileReferenceSupport(
commandManager: CommandManager,
referencesProvider: MdReferencesProvider
client: BaseLanguageClient,
): vscode.Disposable {
return commandManager.register(new FindFileReferencesCommand(referencesProvider));
return commandManager.register(new FindFileReferencesCommand(client));
}

View file

@ -1,369 +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 { dirname, resolve } from 'path';
import * as vscode from 'vscode';
import { IMdParser } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { getLine, ITextDocument } from '../types/textDocument';
import { resolveUriToMarkdownFile } from '../util/openDocumentLink';
import { Schemes } from '../util/schemes';
import { IMdWorkspace } from '../workspace';
import { MdLinkProvider } from './documentLinks';
enum CompletionContextKind {
/** `[...](|)` */
Link,
/** `[...][|]` */
ReferenceLink,
/** `[]: |` */
LinkDefinition,
}
interface AnchorContext {
/**
* Link text before the `#`.
*
* For `[text](xy#z|abc)` this is `xy`.
*/
readonly beforeAnchor: string;
/**
* Text of the anchor before the current position.
*
* For `[text](xy#z|abc)` this is `z`.
*/
readonly anchorPrefix: string;
}
interface CompletionContext {
readonly kind: CompletionContextKind;
/**
* Text of the link before the current position
*
* For `[text](xy#z|abc)` this is `xy#z`.
*/
readonly linkPrefix: string;
/**
* Position of the start of the link.
*
* For `[text](xy#z|abc)` this is the position before `xy`.
*/
readonly linkTextStartPosition: vscode.Position;
/**
* Text of the link after the current position.
*
* For `[text](xy#z|abc)` this is `abc`.
*/
readonly linkSuffix: string;
/**
* Info if the link looks like it is for an anchor: `[](#header)`
*/
readonly anchorInfo?: AnchorContext;
/**
* Indicates that the completion does not require encoding.
*/
readonly skipEncoding?: boolean;
}
function tryDecodeUriComponent(str: string): string {
try {
return decodeURIComponent(str);
} catch {
return str;
}
}
/**
* Adds path completions in markdown files by implementing {@link vscode.CompletionItemProvider}.
*/
export class MdVsCodePathCompletionProvider implements vscode.CompletionItemProvider {
constructor(
private readonly workspace: IMdWorkspace,
private readonly parser: IMdParser,
private readonly linkProvider: MdLinkProvider,
) { }
public async provideCompletionItems(document: ITextDocument, position: vscode.Position, _token: vscode.CancellationToken, _context: vscode.CompletionContext): Promise<vscode.CompletionItem[]> {
if (!this.arePathSuggestionEnabled(document)) {
return [];
}
const context = this.getPathCompletionContext(document, position);
if (!context) {
return [];
}
switch (context.kind) {
case CompletionContextKind.ReferenceLink: {
const items: vscode.CompletionItem[] = [];
for await (const item of this.provideReferenceSuggestions(document, position, context)) {
items.push(item);
}
return items;
}
case CompletionContextKind.LinkDefinition:
case CompletionContextKind.Link: {
const items: vscode.CompletionItem[] = [];
const isAnchorInCurrentDoc = context.anchorInfo && context.anchorInfo.beforeAnchor.length === 0;
// Add anchor #links in current doc
if (context.linkPrefix.length === 0 || isAnchorInCurrentDoc) {
const insertRange = new vscode.Range(context.linkTextStartPosition, position);
for await (const item of this.provideHeaderSuggestions(document, position, context, insertRange)) {
items.push(item);
}
}
if (!isAnchorInCurrentDoc) {
if (context.anchorInfo) { // Anchor to a different document
const rawUri = this.resolveReference(document, context.anchorInfo.beforeAnchor);
if (rawUri) {
const otherDoc = await resolveUriToMarkdownFile(this.workspace, rawUri);
if (otherDoc) {
const anchorStartPosition = position.translate({ characterDelta: -(context.anchorInfo.anchorPrefix.length + 1) });
const range = new vscode.Range(anchorStartPosition, position);
for await (const item of this.provideHeaderSuggestions(otherDoc, position, context, range)) {
items.push(item);
}
}
}
} else { // Normal path suggestions
for await (const item of this.providePathSuggestions(document, position, context)) {
items.push(item);
}
}
}
return items;
}
}
}
private arePathSuggestionEnabled(document: ITextDocument): boolean {
const config = vscode.workspace.getConfiguration('markdown', document.uri);
return config.get('suggest.paths.enabled', true);
}
/// [...](...|
private readonly linkStartPattern = /\[([^\]]*?)\]\(\s*(<[^\>\)]*|[^\s\(\)]*)$/;
/// [...][...|
private readonly referenceLinkStartPattern = /\[([^\]]*?)\]\[\s*([^\s\(\)]*)$/;
/// [id]: |
private readonly definitionPattern = /^\s*\[[\w\-]+\]:\s*([^\s]*)$/m;
private getPathCompletionContext(document: ITextDocument, position: vscode.Position): CompletionContext | undefined {
const line = getLine(document, position.line);
const linePrefixText = line.slice(0, position.character);
const lineSuffixText = line.slice(position.character);
const linkPrefixMatch = linePrefixText.match(this.linkStartPattern);
if (linkPrefixMatch) {
const isAngleBracketLink = linkPrefixMatch[2].startsWith('<');
const prefix = linkPrefixMatch[2].slice(isAngleBracketLink ? 1 : 0);
if (this.refLooksLikeUrl(prefix)) {
return undefined;
}
const suffix = lineSuffixText.match(/^[^\)\s][^\)\s\>]*/);
return {
kind: CompletionContextKind.Link,
linkPrefix: tryDecodeUriComponent(prefix),
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
linkSuffix: suffix ? suffix[0] : '',
anchorInfo: this.getAnchorContext(prefix),
skipEncoding: isAngleBracketLink,
};
}
const definitionLinkPrefixMatch = linePrefixText.match(this.definitionPattern);
if (definitionLinkPrefixMatch) {
const isAngleBracketLink = definitionLinkPrefixMatch[1].startsWith('<');
const prefix = definitionLinkPrefixMatch[1].slice(isAngleBracketLink ? 1 : 0);
if (this.refLooksLikeUrl(prefix)) {
return undefined;
}
const suffix = lineSuffixText.match(/^[^\s]*/);
return {
kind: CompletionContextKind.LinkDefinition,
linkPrefix: tryDecodeUriComponent(prefix),
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
linkSuffix: suffix ? suffix[0] : '',
anchorInfo: this.getAnchorContext(prefix),
skipEncoding: isAngleBracketLink,
};
}
const referenceLinkPrefixMatch = linePrefixText.match(this.referenceLinkStartPattern);
if (referenceLinkPrefixMatch) {
const prefix = referenceLinkPrefixMatch[2];
const suffix = lineSuffixText.match(/^[^\]\s]*/);
return {
kind: CompletionContextKind.ReferenceLink,
linkPrefix: prefix,
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
linkSuffix: suffix ? suffix[0] : '',
};
}
return undefined;
}
/**
* Check if {@param ref} looks like a 'http:' style url.
*/
private refLooksLikeUrl(prefix: string): boolean {
return /^\s*[\w\d\-]+:/.test(prefix);
}
private getAnchorContext(prefix: string): AnchorContext | undefined {
const anchorMatch = prefix.match(/^(.*)#([\w\d\-]*)$/);
if (!anchorMatch) {
return undefined;
}
return {
beforeAnchor: anchorMatch[1],
anchorPrefix: anchorMatch[2],
};
}
private async *provideReferenceSuggestions(document: ITextDocument, position: vscode.Position, context: CompletionContext): AsyncIterable<vscode.CompletionItem> {
const insertionRange = new vscode.Range(context.linkTextStartPosition, position);
const replacementRange = new vscode.Range(insertionRange.start, position.translate({ characterDelta: context.linkSuffix.length }));
const { definitions } = await this.linkProvider.getLinks(document);
for (const [_, def] of definitions) {
yield {
kind: vscode.CompletionItemKind.Reference,
label: def.ref.text,
range: {
inserting: insertionRange,
replacing: replacementRange,
},
};
}
}
private async *provideHeaderSuggestions(document: ITextDocument, position: vscode.Position, context: CompletionContext, insertionRange: vscode.Range): AsyncIterable<vscode.CompletionItem> {
const toc = await TableOfContents.createForDocumentOrNotebook(this.parser, document);
for (const entry of toc.entries) {
const replacementRange = new vscode.Range(insertionRange.start, position.translate({ characterDelta: context.linkSuffix.length }));
yield {
kind: vscode.CompletionItemKind.Reference,
label: '#' + decodeURIComponent(entry.slug.value),
range: {
inserting: insertionRange,
replacing: replacementRange,
},
};
}
}
private async *providePathSuggestions(document: ITextDocument, position: vscode.Position, context: CompletionContext): AsyncIterable<vscode.CompletionItem> {
const valueBeforeLastSlash = context.linkPrefix.substring(0, context.linkPrefix.lastIndexOf('/') + 1); // keep the last slash
const parentDir = this.resolveReference(document, valueBeforeLastSlash || '.');
if (!parentDir) {
return;
}
const pathSegmentStart = position.translate({ characterDelta: valueBeforeLastSlash.length - context.linkPrefix.length });
const insertRange = new vscode.Range(pathSegmentStart, position);
const pathSegmentEnd = position.translate({ characterDelta: context.linkSuffix.length });
const replacementRange = new vscode.Range(pathSegmentStart, pathSegmentEnd);
let dirInfo: [string, vscode.FileType][];
try {
dirInfo = await this.workspace.readDirectory(parentDir);
} catch {
return;
}
for (const [name, type] of dirInfo) {
// Exclude paths that start with `.`
if (name.startsWith('.')) {
continue;
}
const isDir = type === vscode.FileType.Directory;
yield {
label: isDir ? name + '/' : name,
insertText: (context.skipEncoding ? name : encodeURIComponent(name)) + (isDir ? '/' : ''),
kind: isDir ? vscode.CompletionItemKind.Folder : vscode.CompletionItemKind.File,
range: {
inserting: insertRange,
replacing: replacementRange,
},
command: isDir ? { command: 'editor.action.triggerSuggest', title: '' } : undefined,
};
}
}
private resolveReference(document: ITextDocument, ref: string): vscode.Uri | undefined {
const docUri = this.getFileUriOfTextDocument(document);
if (ref.startsWith('/')) {
const workspaceFolder = vscode.workspace.getWorkspaceFolder(docUri);
if (workspaceFolder) {
return vscode.Uri.joinPath(workspaceFolder.uri, ref);
} else {
return this.resolvePath(docUri, ref.slice(1));
}
}
return this.resolvePath(docUri, ref);
}
private resolvePath(root: vscode.Uri, ref: string): vscode.Uri | undefined {
try {
if (root.scheme === Schemes.file) {
return vscode.Uri.file(resolve(dirname(root.fsPath), ref));
} else {
return root.with({
path: resolve(dirname(root.path), ref),
});
}
} catch {
return undefined;
}
}
private getFileUriOfTextDocument(document: ITextDocument) {
if (document.uri.scheme === 'vscode-notebook-cell') {
const notebook = vscode.workspace.notebookDocuments
.find(notebook => notebook.getCells().some(cell => cell.document === document));
if (notebook) {
return notebook.uri;
}
}
return document.uri;
}
}
export function registerPathCompletionSupport(
selector: vscode.DocumentSelector,
workspace: IMdWorkspace,
parser: IMdParser,
linkProvider: MdLinkProvider,
): vscode.Disposable {
return vscode.languages.registerCompletionItemProvider(selector, new MdVsCodePathCompletionProvider(workspace, parser, linkProvider), '.', '/', '#');
}

View file

@ -312,30 +312,6 @@ export class MdReferencesProvider extends Disposable {
}
}
/**
* Implements {@link vscode.ReferenceProvider} for markdown documents.
*/
export class MdVsCodeReferencesProvider implements vscode.ReferenceProvider {
public constructor(
private readonly referencesProvider: MdReferencesProvider
) { }
async provideReferences(document: ITextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise<vscode.Location[]> {
const allRefs = await this.referencesProvider.getReferencesAtPosition(document, position, token);
return allRefs
.filter(ref => context.includeDeclaration || !ref.isDefinition)
.map(ref => ref.location);
}
}
export function registerReferencesSupport(
selector: vscode.DocumentSelector,
referencesProvider: MdReferencesProvider,
): vscode.Disposable {
return vscode.languages.registerReferenceProvider(selector, new MdVsCodeReferencesProvider(referencesProvider));
}
export async function tryResolveLinkPath(originalUri: vscode.Uri, workspace: IMdWorkspace): Promise<vscode.Uri | undefined> {
if (await workspace.pathExists(originalUri)) {
return originalUri;

View file

@ -1,281 +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 path from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as URI from 'vscode-uri';
import { Slugifier } from '../slugify';
import { ITextDocument } from '../types/textDocument';
import { Disposable } from '../util/dispose';
import { resolveDocumentLink } from '../util/openDocumentLink';
import { IMdWorkspace } from '../workspace';
import { InternalHref } from './documentLinks';
import { MdHeaderReference, MdLinkReference, MdReference, MdReferencesProvider, tryResolveLinkPath } from './references';
const localize = nls.loadMessageBundle();
export interface MdReferencesResponse {
references: MdReference[];
triggerRef: MdReference;
}
interface MdFileRenameEdit {
readonly from: vscode.Uri;
readonly to: vscode.Uri;
}
/**
* Type with additional metadata about the edits for testing
*
* This is needed since `vscode.WorkspaceEdit` does not expose info on file renames.
*/
export interface MdWorkspaceEdit {
readonly edit: vscode.WorkspaceEdit;
readonly fileRenames?: ReadonlyArray<MdFileRenameEdit>;
}
function tryDecodeUri(str: string): string {
try {
return decodeURI(str);
} catch {
return str;
}
}
export class MdVsCodeRenameProvider extends Disposable implements vscode.RenameProvider {
private cachedRefs?: {
readonly resource: vscode.Uri;
readonly version: number;
readonly position: vscode.Position;
readonly triggerRef: MdReference;
readonly references: MdReference[];
} | undefined;
private readonly renameNotSupportedText = localize('invalidRenameLocation', "Rename not supported at location");
public constructor(
private readonly workspace: IMdWorkspace,
private readonly referencesProvider: MdReferencesProvider,
private readonly slugifier: Slugifier,
) {
super();
}
public async prepareRename(document: ITextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<undefined | { readonly range: vscode.Range; readonly placeholder: string }> {
const allRefsInfo = await this.getAllReferences(document, position, token);
if (token.isCancellationRequested) {
return undefined;
}
if (!allRefsInfo || !allRefsInfo.references.length) {
throw new Error(this.renameNotSupportedText);
}
const triggerRef = allRefsInfo.triggerRef;
switch (triggerRef.kind) {
case 'header': {
return { range: triggerRef.headerTextLocation.range, placeholder: triggerRef.headerText };
}
case 'link': {
if (triggerRef.link.kind === 'definition') {
// We may have been triggered on the ref or the definition itself
if (triggerRef.link.ref.range.contains(position)) {
return { range: triggerRef.link.ref.range, placeholder: triggerRef.link.ref.text };
}
}
if (triggerRef.link.href.kind === 'external') {
return { range: triggerRef.link.source.hrefRange, placeholder: document.getText(triggerRef.link.source.hrefRange) };
}
// See if we are renaming the fragment or the path
const { fragmentRange } = triggerRef.link.source;
if (fragmentRange?.contains(position)) {
const declaration = this.findHeaderDeclaration(allRefsInfo.references);
if (declaration) {
return { range: fragmentRange, placeholder: declaration.headerText };
}
return { range: fragmentRange, placeholder: document.getText(fragmentRange) };
}
const range = this.getFilePathRange(triggerRef);
if (!range) {
throw new Error(this.renameNotSupportedText);
}
return { range, placeholder: tryDecodeUri(document.getText(range)) };
}
}
}
private getFilePathRange(ref: MdLinkReference): vscode.Range {
if (ref.link.source.fragmentRange) {
return ref.link.source.hrefRange.with(undefined, ref.link.source.fragmentRange.start.translate(0, -1));
}
return ref.link.source.hrefRange;
}
private findHeaderDeclaration(references: readonly MdReference[]): MdHeaderReference | undefined {
return references.find(ref => ref.isDefinition && ref.kind === 'header') as MdHeaderReference | undefined;
}
public async provideRenameEdits(document: ITextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise<vscode.WorkspaceEdit | undefined> {
return (await this.provideRenameEditsImpl(document, position, newName, token))?.edit;
}
public async provideRenameEditsImpl(document: ITextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise<MdWorkspaceEdit | undefined> {
const allRefsInfo = await this.getAllReferences(document, position, token);
if (token.isCancellationRequested || !allRefsInfo || !allRefsInfo.references.length) {
return undefined;
}
const triggerRef = allRefsInfo.triggerRef;
if (triggerRef.kind === 'link' && (
(triggerRef.link.kind === 'definition' && triggerRef.link.ref.range.contains(position)) || triggerRef.link.href.kind === 'reference'
)) {
return this.renameReferenceLinks(allRefsInfo, newName);
} else if (triggerRef.kind === 'link' && triggerRef.link.href.kind === 'external') {
return this.renameExternalLink(allRefsInfo, newName);
} else if (triggerRef.kind === 'header' || (triggerRef.kind === 'link' && triggerRef.link.source.fragmentRange?.contains(position) && (triggerRef.link.kind === 'definition' || triggerRef.link.kind === 'link' && triggerRef.link.href.kind === 'internal'))) {
return this.renameFragment(allRefsInfo, newName);
} else if (triggerRef.kind === 'link' && !triggerRef.link.source.fragmentRange?.contains(position) && (triggerRef.link.kind === 'link' || triggerRef.link.kind === 'definition') && triggerRef.link.href.kind === 'internal') {
return this.renameFilePath(triggerRef.link.source.resource, triggerRef.link.href, allRefsInfo, newName);
}
return undefined;
}
private async renameFilePath(triggerDocument: vscode.Uri, triggerHref: InternalHref, allRefsInfo: MdReferencesResponse, newName: string): Promise<MdWorkspaceEdit> {
const edit = new vscode.WorkspaceEdit();
const fileRenames: MdFileRenameEdit[] = [];
const targetUri = await tryResolveLinkPath(triggerHref.path, this.workspace) ?? triggerHref.path;
const rawNewFilePath = resolveDocumentLink(newName, triggerDocument);
let resolvedNewFilePath = rawNewFilePath;
if (!URI.Utils.extname(resolvedNewFilePath)) {
// If the newly entered path doesn't have a file extension but the original file did
// tack on a .md file extension
if (URI.Utils.extname(targetUri)) {
resolvedNewFilePath = resolvedNewFilePath.with({
path: resolvedNewFilePath.path + '.md'
});
}
}
// First rename the file
if (await this.workspace.pathExists(targetUri)) {
fileRenames.push({ from: targetUri, to: resolvedNewFilePath });
edit.renameFile(targetUri, resolvedNewFilePath);
}
// Then update all refs to it
for (const ref of allRefsInfo.references) {
if (ref.kind === 'link') {
// Try to preserve style of existing links
let newPath: string;
if (ref.link.source.hrefText.startsWith('/')) {
const root = resolveDocumentLink('/', ref.link.source.resource);
newPath = '/' + path.relative(root.toString(true), rawNewFilePath.toString(true));
} else {
const rootDir = URI.Utils.dirname(ref.link.source.resource);
if (rootDir.scheme === rawNewFilePath.scheme && rootDir.scheme !== 'untitled') {
newPath = path.relative(rootDir.toString(true), rawNewFilePath.toString(true));
if (newName.startsWith('./') && !newPath.startsWith('../') || newName.startsWith('.\\') && !newPath.startsWith('..\\')) {
newPath = './' + newPath;
}
} else {
newPath = newName;
}
}
edit.replace(ref.link.source.resource, this.getFilePathRange(ref), encodeURI(newPath.replace(/\\/g, '/')));
}
}
return { edit, fileRenames };
}
private renameFragment(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit {
const slug = this.slugifier.fromHeading(newName).value;
const edit = new vscode.WorkspaceEdit();
for (const ref of allRefsInfo.references) {
switch (ref.kind) {
case 'header':
edit.replace(ref.location.uri, ref.headerTextLocation.range, newName);
break;
case 'link':
edit.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, !ref.link.source.fragmentRange || ref.link.href.kind === 'external' ? newName : slug);
break;
}
}
return { edit };
}
private renameExternalLink(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit {
const edit = new vscode.WorkspaceEdit();
for (const ref of allRefsInfo.references) {
if (ref.kind === 'link') {
edit.replace(ref.link.source.resource, ref.location.range, newName);
}
}
return { edit };
}
private renameReferenceLinks(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit {
const edit = new vscode.WorkspaceEdit();
for (const ref of allRefsInfo.references) {
if (ref.kind === 'link') {
if (ref.link.kind === 'definition') {
edit.replace(ref.link.source.resource, ref.link.ref.range, newName);
} else {
edit.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, newName);
}
}
}
return { edit };
}
private async getAllReferences(document: ITextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReferencesResponse | undefined> {
const version = document.version;
if (this.cachedRefs
&& this.cachedRefs.resource.fsPath === document.uri.fsPath
&& this.cachedRefs.version === document.version
&& this.cachedRefs.position.isEqual(position)
) {
return this.cachedRefs;
}
const references = await this.referencesProvider.getReferencesAtPosition(document, position, token);
const triggerRef = references.find(ref => ref.isTriggerLocation);
if (!triggerRef) {
return undefined;
}
this.cachedRefs = {
resource: document.uri,
version,
position,
references,
triggerRef
};
return this.cachedRefs;
}
}
export function registerRenameSupport(
selector: vscode.DocumentSelector,
workspace: IMdWorkspace,
referencesProvider: MdReferencesProvider,
slugifier: Slugifier,
): vscode.Disposable {
return vscode.languages.registerRenameProvider(selector, new MdVsCodeRenameProvider(workspace, referencesProvider, slugifier));
}

View file

@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Token = require('markdown-it/lib/token');
import { RequestType } from 'vscode-languageclient';
import type * as lsp from 'vscode-languageserver-types';
// 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');
// To server
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');

View file

@ -1,144 +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 { MdVsCodeDefinitionProvider } from '../languageFeatures/definitions';
import { MdReferencesProvider } from '../languageFeatures/references';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { DisposableStore } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { IMdWorkspace } from '../workspace';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { joinLines, withStore, workspacePath } from './util';
function getDefinition(store: DisposableStore, doc: InMemoryDocument, pos: vscode.Position, workspace: IMdWorkspace) {
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const referencesProvider = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger));
const provider = new MdVsCodeDefinitionProvider(referencesProvider);
return provider.provideDefinition(doc, pos, noopToken);
}
function assertDefinitionsEqual(actualDef: vscode.Definition, ...expectedDefs: { uri: vscode.Uri; line: number; startCharacter?: number; endCharacter?: number }[]) {
const actualDefsArr = Array.isArray(actualDef) ? actualDef : [actualDef];
assert.strictEqual(actualDefsArr.length, expectedDefs.length, `Definition counts should match`);
for (let i = 0; i < actualDefsArr.length; ++i) {
const actual = actualDefsArr[i];
const expected = expectedDefs[i];
assert.strictEqual(actual.uri.toString(), expected.uri.toString(), `Definition '${i}' has expected document`);
assert.strictEqual(actual.range.start.line, expected.line, `Definition '${i}' has expected start line`);
assert.strictEqual(actual.range.end.line, expected.line, `Definition '${i}' has expected end line`);
if (typeof expected.startCharacter !== 'undefined') {
assert.strictEqual(actual.range.start.character, expected.startCharacter, `Definition '${i}' has expected start character`);
}
if (typeof expected.endCharacter !== 'undefined') {
assert.strictEqual(actual.range.end.character, expected.endCharacter, `Definition '${i}' has expected end character`);
}
}
}
suite('markdown: Go to definition', () => {
test('Should not return definition when on link text', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`[ref](#abc)`,
`[ref]: http://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const defs = await getDefinition(store, doc, new vscode.Position(0, 1), workspace);
assert.deepStrictEqual(defs, undefined);
}));
test('Should find definition links within file from link', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`, // trigger here
``,
`[abc]: https://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const defs = await getDefinition(store, doc, new vscode.Position(0, 12), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 2 },
);
}));
test('Should find definition links using shorthand', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[ref]`, // trigger 1
``,
`[yes][ref]`, // trigger 2
``,
`[ref]: /Hello.md` // trigger 3
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
{
const defs = await getDefinition(store, doc, new vscode.Position(0, 2), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 4 },
);
}
{
const defs = await getDefinition(store, doc, new vscode.Position(2, 7), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 4 },
);
}
{
const defs = await getDefinition(store, doc, new vscode.Position(4, 2), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 4 },
);
}
}));
test('Should find definition links within file from definition', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com`, // trigger here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const defs = await getDefinition(store, doc, new vscode.Position(2, 3), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 2 },
);
}));
test('Should not find definition links across files', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(workspacePath('other.md'), joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com?bad`
))
]));
const defs = await getDefinition(store, doc, new vscode.Position(0, 12), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 2 },
);
}));
});

View file

@ -24,7 +24,7 @@ function workspaceFile(...segments: string[]) {
async function getLinksForFile(file: vscode.Uri): Promise<vscode.DocumentLink[]> {
debugLog('getting links', file.toString(), Date.now());
const r = (await vscode.commands.executeCommand<vscode.DocumentLink[]>('vscode.executeLinkProvider', file))!;
const r = (await vscode.commands.executeCommand<vscode.DocumentLink[]>('vscode.executeLinkProvider', file, /*linkResolveCount*/ 100))!;
debugLog('got links', file.toString(), Date.now());
return r;
}
@ -134,7 +134,7 @@ async function getLinksForFile(file: vscode.Uri): Promise<vscode.DocumentLink[]>
}
});
test('Should navigate to fragment within current untitled file', async () => {
test('Should navigate to fragment within current untitled file', async () => { // TODO: skip for now for ls migration
const testFile = workspaceFile('x.md').with({ scheme: 'untitled' });
await withFileContents(testFile, joinLines(
'[](#second)',
@ -171,7 +171,7 @@ async function withFileContents(file: vscode.Uri, contents: string): Promise<voi
async function executeLink(link: vscode.DocumentLink) {
debugLog('executeingLink', link.target?.toString(), Date.now());
const args = JSON.parse(decodeURIComponent(link.target!.query));
await vscode.commands.executeCommand(link.target!.path, args);
const args: any[] = JSON.parse(decodeURIComponent(link.target!.query));
await vscode.commands.executeCommand(link.target!.path, vscode.Uri.from(args[0]), ...args.slice(1));
debugLog('executedLink', vscode.window.activeTextEditor?.document.toString(), Date.now());
}

View file

@ -1,539 +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 { MdLink, MdLinkComputer, MdLinkProvider, MdVsCodeLinkProvider } from '../languageFeatures/documentLinks';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { assertRangeEqual, joinLines, workspacePath } from './util';
suite('Markdown: MdLinkComputer', () => {
function getLinksForFile(fileContents: string): Promise<MdLink[]> {
const doc = new InMemoryDocument(workspacePath('x.md'), fileContents);
const engine = createNewMarkdownEngine();
const linkProvider = new MdLinkComputer(engine);
return linkProvider.getAllLinks(doc, noopToken);
}
function assertLinksEqual(actualLinks: readonly MdLink[], expected: ReadonlyArray<vscode.Range | { readonly range: vscode.Range; readonly sourceText: string }>) {
assert.strictEqual(actualLinks.length, expected.length);
for (let i = 0; i < actualLinks.length; ++i) {
const exp = expected[i];
if ('range' in exp) {
assertRangeEqual(actualLinks[i].source.hrefRange, exp.range, `Range ${i} to be equal`);
assert.strictEqual(actualLinks[i].source.hrefText, exp.sourceText, `Source text ${i} to be equal`);
} else {
assertRangeEqual(actualLinks[i].source.hrefRange, exp, `Range ${i} to be equal`);
}
}
}
test('Should not return anything for empty document', async () => {
const links = await getLinksForFile('');
assertLinksEqual(links, []);
});
test('Should not return anything for simple document without links', async () => {
const links = await getLinksForFile(joinLines(
'# a',
'fdasfdfsafsa',
));
assertLinksEqual(links, []);
});
test('Should detect basic http links', async () => {
const links = await getLinksForFile('a [b](https://example.com) c');
assertLinksEqual(links, [
new vscode.Range(0, 6, 0, 25)
]);
});
test('Should detect basic workspace links', async () => {
{
const links = await getLinksForFile('a [b](./file) c');
assertLinksEqual(links, [
new vscode.Range(0, 6, 0, 12)
]);
}
{
const links = await getLinksForFile('a [b](file.png) c');
assertLinksEqual(links, [
new vscode.Range(0, 6, 0, 14)
]);
}
});
test('Should detect links with title', async () => {
const links = await getLinksForFile('a [b](https://example.com "abc") c');
assertLinksEqual(links, [
new vscode.Range(0, 6, 0, 25)
]);
});
test('Should handle links with escaped characters in name (#35245)', async () => {
const links = await getLinksForFile('a [b\\]](./file)');
assertLinksEqual(links, [
new vscode.Range(0, 8, 0, 14)
]);
});
test('Should handle links with balanced parens', async () => {
{
const links = await getLinksForFile('a [b](https://example.com/a()c) c');
assertLinksEqual(links, [
new vscode.Range(0, 6, 0, 30)
]);
}
{
const links = await getLinksForFile('a [b](https://example.com/a(b)c) c');
assertLinksEqual(links, [
new vscode.Range(0, 6, 0, 31)
]);
}
{
// #49011
const links = await getLinksForFile('[A link](http://ThisUrlhasParens/A_link(in_parens))');
assertLinksEqual(links, [
new vscode.Range(0, 9, 0, 50)
]);
}
});
test('Should ignore bracketed text inside link title (#150921)', async () => {
{
const links = await getLinksForFile('[some [inner] in title](link)');
assertLinksEqual(links, [
new vscode.Range(0, 24, 0, 28),
]);
}
{
const links = await getLinksForFile('[some [inner] in title](<link>)');
assertLinksEqual(links, [
new vscode.Range(0, 25, 0, 29),
]);
}
{
const links = await getLinksForFile('[some [inner with space] in title](link)');
assertLinksEqual(links, [
new vscode.Range(0, 35, 0, 39),
]);
}
{
const links = await getLinksForFile(joinLines(
`# h`,
`[[a]](http://example.com)`,
));
assertLinksEqual(links, [
new vscode.Range(1, 6, 1, 24),
]);
}
});
test('Should handle two links without space', async () => {
const links = await getLinksForFile('a ([test](test)[test2](test2)) c');
assertLinksEqual(links, [
new vscode.Range(0, 10, 0, 14),
new vscode.Range(0, 23, 0, 28)
]);
});
test('should handle hyperlinked images (#49238)', async () => {
{
const links = await getLinksForFile('[![alt text](image.jpg)](https://example.com)');
assertLinksEqual(links, [
new vscode.Range(0, 25, 0, 44),
new vscode.Range(0, 13, 0, 22),
]);
}
{
const links = await getLinksForFile('[![a]( whitespace.jpg )]( https://whitespace.com )');
assertLinksEqual(links, [
new vscode.Range(0, 26, 0, 48),
new vscode.Range(0, 7, 0, 21),
]);
}
{
const links = await getLinksForFile('[![a](img1.jpg)](file1.txt) text [![a](img2.jpg)](file2.txt)');
assertLinksEqual(links, [
new vscode.Range(0, 17, 0, 26),
new vscode.Range(0, 6, 0, 14),
new vscode.Range(0, 50, 0, 59),
new vscode.Range(0, 39, 0, 47),
]);
}
});
test('Should not consider link references starting with ^ character valid (#107471)', async () => {
const links = await getLinksForFile('[^reference]: https://example.com');
assertLinksEqual(links, []);
});
test('Should find definitions links with spaces in angle brackets (#136073)', async () => {
const links = await getLinksForFile(joinLines(
'[a]: <b c>',
'[b]: <cd>',
));
assertLinksEqual(links, [
{ range: new vscode.Range(0, 6, 0, 9), sourceText: 'b c' },
{ range: new vscode.Range(1, 6, 1, 8), sourceText: 'cd' },
]);
});
test('Should only find one link for reference sources [a]: source (#141285)', async () => {
const links = await getLinksForFile(joinLines(
'[Works]: https://example.com',
));
assertLinksEqual(links, [
{ range: new vscode.Range(0, 9, 0, 28), sourceText: 'https://example.com' },
]);
});
test('Should find reference link shorthand (#141285)', async () => {
const links = await getLinksForFile(joinLines(
'[ref]',
'[ref]: https://example.com',
));
assertLinksEqual(links, [
{ range: new vscode.Range(0, 1, 0, 4), sourceText: 'ref' },
{ range: new vscode.Range(1, 7, 1, 26), sourceText: 'https://example.com' },
]);
});
test('Should find reference link shorthand using empty closing brackets (#141285)', async () => {
const links = await getLinksForFile(joinLines(
'[ref][]',
));
assertLinksEqual(links, [
new vscode.Range(0, 1, 0, 4),
]);
});
test.skip('Should find reference link shorthand for link with space in label (#141285)', async () => {
const links = await getLinksForFile(joinLines(
'[ref with space]',
));
assertLinksEqual(links, [
new vscode.Range(0, 7, 0, 26),
]);
});
test('Should not include reference links with escaped leading brackets', async () => {
const links = await getLinksForFile(joinLines(
`\\[bad link][good]`,
`\\[good]`,
`[good]: http://example.com`,
));
assertLinksEqual(links, [
new vscode.Range(2, 8, 2, 26) // Should only find the definition
]);
});
test('Should not consider links in code fenced with backticks', async () => {
const links = await getLinksForFile(joinLines(
'```',
'[b](https://example.com)',
'```'));
assertLinksEqual(links, []);
});
test('Should not consider links in code fenced with tilde', async () => {
const links = await getLinksForFile(joinLines(
'~~~',
'[b](https://example.com)',
'~~~'));
assertLinksEqual(links, []);
});
test('Should not consider links in indented code', async () => {
const links = await getLinksForFile(' [b](https://example.com)');
assertLinksEqual(links, []);
});
test('Should not consider links in inline code span', async () => {
const links = await getLinksForFile('`[b](https://example.com)`');
assertLinksEqual(links, []);
});
test('Should not consider links with code span inside', async () => {
const links = await getLinksForFile('[li`nk](https://example.com`)');
assertLinksEqual(links, []);
});
test('Should not consider links in multiline inline code span', async () => {
const links = await getLinksForFile(joinLines(
'`` ',
'[b](https://example.com)',
'``'));
assertLinksEqual(links, []);
});
test('Should not consider link references in code fenced with backticks (#146714)', async () => {
const links = await getLinksForFile(joinLines(
'```',
'[a] [bb]',
'```'));
assertLinksEqual(links, []);
});
test('Should not consider reference sources in code fenced with backticks (#146714)', async () => {
const links = await getLinksForFile(joinLines(
'```',
'[a]: http://example.com;',
'[b]: <http://example.com>;',
'[c]: (http://example.com);',
'```'));
assertLinksEqual(links, []);
});
test('Should not consider links in multiline inline code span between between text', async () => {
const links = await getLinksForFile(joinLines(
'[b](https://1.com) `[b](https://2.com)',
'[b](https://3.com) ` [b](https://4.com)'));
assertLinksEqual(links, [
new vscode.Range(0, 4, 0, 17),
new vscode.Range(1, 25, 1, 38),
]);
});
test('Should not consider links in multiline inline code span with new line after the first backtick', async () => {
const links = await getLinksForFile(joinLines(
'`',
'[b](https://example.com)`'));
assertLinksEqual(links, []);
});
test('Should not miss links in invalid multiline inline code span', async () => {
const links = await getLinksForFile(joinLines(
'`` ',
'',
'[b](https://example.com)',
'',
'``'));
assertLinksEqual(links, [
new vscode.Range(2, 4, 2, 23)
]);
});
test('Should find autolinks', async () => {
const links = await getLinksForFile('pre <http://example.com> post');
assertLinksEqual(links, [
new vscode.Range(0, 5, 0, 23)
]);
});
test('Should not detect links inside html comment blocks', async () => {
const links = await getLinksForFile(joinLines(
`<!-- <http://example.com> -->`,
`<!-- [text](./foo.md) -->`,
`<!-- [text]: ./foo.md -->`,
``,
`<!--`,
`<http://example.com>`,
`-->`,
``,
`<!--`,
`[text](./foo.md)`,
`-->`,
``,
`<!--`,
`[text]: ./foo.md`,
`-->`,
));
assertLinksEqual(links, []);
});
test.skip('Should not detect links inside inline html comments', async () => {
// See #149678
const links = await getLinksForFile(joinLines(
`text <!-- <http://example.com> --> text`,
`text <!-- [text](./foo.md) --> text`,
`text <!-- [text]: ./foo.md --> text`,
``,
`text <!--`,
`<http://example.com>`,
`--> text`,
``,
`text <!--`,
`[text](./foo.md)`,
`--> text`,
``,
`text <!--`,
`[text]: ./foo.md`,
`--> text`,
));
assertLinksEqual(links, []);
});
test('Should not mark checkboxes as links', async () => {
const links = await getLinksForFile(joinLines(
'- [x]',
'- [X]',
'- [ ]',
'* [x]',
'* [X]',
'* [ ]',
``,
`[x]: http://example.com`
));
assertLinksEqual(links, [
new vscode.Range(7, 5, 7, 23)
]);
});
test('Should still find links on line with checkbox', async () => {
const links = await getLinksForFile(joinLines(
'- [x] [x]',
'- [X] [x]',
'- [] [x]',
``,
`[x]: http://example.com`
));
assertLinksEqual(links, [
new vscode.Range(0, 7, 0, 8),
new vscode.Range(1, 7, 1, 8),
new vscode.Range(2, 6, 2, 7),
new vscode.Range(4, 5, 4, 23),
]);
});
test('Should find link only within angle brackets.', async () => {
const links = await getLinksForFile(joinLines(
`[link](<path>)`
));
assertLinksEqual(links, [new vscode.Range(0, 8, 0, 12)]);
});
test('Should find link within angle brackets even with link title.', async () => {
const links = await getLinksForFile(joinLines(
`[link](<path> "test title")`
));
assertLinksEqual(links, [new vscode.Range(0, 8, 0, 12)]);
});
test('Should find link within angle brackets even with surrounding spaces.', async () => {
const links = await getLinksForFile(joinLines(
`[link]( <path> )`
));
assertLinksEqual(links, [new vscode.Range(0, 9, 0, 13)]);
});
test('Should find link within angle brackets for image hyperlinks.', async () => {
const links = await getLinksForFile(joinLines(
`![link](<path>)`
));
assertLinksEqual(links, [new vscode.Range(0, 9, 0, 13)]);
});
test('Should find link with spaces in angle brackets for image hyperlinks with titles.', async () => {
const links = await getLinksForFile(joinLines(
`![link](< path > "test")`
));
assertLinksEqual(links, [new vscode.Range(0, 9, 0, 15)]);
});
test('Should not find link due to incorrect angle bracket notation or usage.', async () => {
const links = await getLinksForFile(joinLines(
`[link](<path )`,
`[link](<> path>)`,
`[link](> path)`,
));
assertLinksEqual(links, []);
});
test('Should find link within angle brackets even with space inside link.', async () => {
const links = await getLinksForFile(joinLines(
`[link](<pa th>)`
));
assertLinksEqual(links, [new vscode.Range(0, 8, 0, 13)]);
});
test('Should find links with titles', async () => {
const links = await getLinksForFile(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))`,
));
assertLinksEqual(links, [
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 not include link with empty angle bracket', async () => {
const links = await getLinksForFile(joinLines(
`[](<>)`,
`[link](<>)`,
`[link](<> "text")`,
`[link](<> 'text')`,
`[link](<> (text))`,
));
assertLinksEqual(links, []);
});
});
suite('Markdown: VS Code DocumentLinkProvider', () => {
function getLinksForFile(fileContents: string) {
const doc = new InMemoryDocument(workspacePath('x.md'), fileContents);
const workspace = new InMemoryMdWorkspace([doc]);
const engine = createNewMarkdownEngine();
const linkProvider = new MdLinkProvider(engine, workspace, nulLogger);
const provider = new MdVsCodeLinkProvider(linkProvider);
return provider.provideDocumentLinks(doc, noopToken);
}
function assertLinksEqual(actualLinks: readonly vscode.DocumentLink[], expectedRanges: readonly vscode.Range[]) {
assert.strictEqual(actualLinks.length, expectedRanges.length);
for (let i = 0; i < actualLinks.length; ++i) {
assertRangeEqual(actualLinks[i].range, expectedRanges[i], `Range ${i} to be equal`);
}
}
test('Should include defined reference links (#141285)', async () => {
const links = await getLinksForFile(joinLines(
'[ref]',
'[ref][]',
'[ref][ref]',
'',
'[ref]: http://example.com'
));
assertLinksEqual(links, [
new vscode.Range(0, 1, 0, 4),
new vscode.Range(1, 1, 1, 4),
new vscode.Range(2, 6, 2, 9),
new vscode.Range(4, 7, 4, 25),
]);
});
test('Should not include reference link shorthand when definition does not exist (#141285)', async () => {
const links = await getLinksForFile('[ref]');
assertLinksEqual(links, []);
});
});

View file

@ -1,120 +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 { MdReference, MdReferencesProvider } from '../languageFeatures/references';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { DisposableStore } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { IMdWorkspace } from '../workspace';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { joinLines, withStore, workspacePath } from './util';
function getFileReferences(store: DisposableStore, resource: vscode.Uri, workspace: IMdWorkspace) {
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const computer = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger));
return computer.getReferencesToFileInWorkspace(resource, noopToken);
}
function assertReferencesEqual(actualRefs: readonly MdReference[], ...expectedRefs: { uri: vscode.Uri; line: number }[]) {
assert.strictEqual(actualRefs.length, expectedRefs.length, `Reference counts should match`);
for (let i = 0; i < actualRefs.length; ++i) {
const actual = actualRefs[i].location;
const expected = expectedRefs[i];
assert.strictEqual(actual.uri.toString(), expected.uri.toString(), `Ref '${i}' has expected document`);
assert.strictEqual(actual.range.start.line, expected.line, `Ref '${i}' has expected start line`);
assert.strictEqual(actual.range.end.line, expected.line, `Ref '${i}' has expected end line`);
}
}
suite('markdown: find file references', () => {
test('Should find basic references', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const workspace = store.add(new InMemoryMdWorkspace([
new InMemoryDocument(docUri, joinLines(
`# header`,
`[link 1](./other.md)`,
`[link 2](./other.md)`
)),
new InMemoryDocument(otherUri, joinLines(
`# header`,
`pre`,
`[link 3](./other.md)`,
`post`
)),
]));
const refs = await getFileReferences(store, otherUri, workspace);
assertReferencesEqual(refs,
{ uri: docUri, line: 1 },
{ uri: docUri, line: 2 },
{ uri: otherUri, line: 2 },
);
}));
test('Should find references with and without file extensions', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const workspace = store.add(new InMemoryMdWorkspace([
new InMemoryDocument(docUri, joinLines(
`# header`,
`[link 1](./other.md)`,
`[link 2](./other)`
)),
new InMemoryDocument(otherUri, joinLines(
`# header`,
`pre`,
`[link 3](./other.md)`,
`[link 4](./other)`,
`post`
)),
]));
const refs = await getFileReferences(store, otherUri, workspace);
assertReferencesEqual(refs,
{ uri: docUri, line: 1 },
{ uri: docUri, line: 2 },
{ uri: otherUri, line: 2 },
{ uri: otherUri, line: 3 },
);
}));
test('Should find references with headers on links', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const workspace = store.add(new InMemoryMdWorkspace([
new InMemoryDocument(docUri, joinLines(
`# header`,
`[link 1](./other.md#sub-bla)`,
`[link 2](./other#sub-bla)`
)),
new InMemoryDocument(otherUri, joinLines(
`# header`,
`pre`,
`[link 3](./other.md#sub-bla)`,
`[link 4](./other#sub-bla)`,
`post`
)),
]));
const refs = await getFileReferences(store, otherUri, workspace);
assertReferencesEqual(refs,
{ uri: docUri, line: 1 },
{ uri: docUri, line: 2 },
{ uri: otherUri, line: 2 },
{ uri: otherUri, line: 3 },
);
}));
});

View file

@ -1,313 +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 { MdLinkProvider } from '../languageFeatures/documentLinks';
import { MdVsCodePathCompletionProvider } from '../languageFeatures/pathCompletions';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { IMdWorkspace } from '../workspace';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { CURSOR, getCursorPositions, joinLines, workspacePath } from './util';
async function getCompletionsAtCursor(resource: vscode.Uri, fileContents: string, workspace?: IMdWorkspace) {
const doc = new InMemoryDocument(resource, fileContents);
const engine = createNewMarkdownEngine();
const ws = workspace ?? new InMemoryMdWorkspace([doc]);
const linkProvider = new MdLinkProvider(engine, ws, nulLogger);
const provider = new MdVsCodePathCompletionProvider(ws, engine, linkProvider);
const cursorPositions = getCursorPositions(fileContents, doc);
const completions = await provider.provideCompletionItems(doc, cursorPositions[0], noopToken, {
triggerCharacter: undefined,
triggerKind: vscode.CompletionTriggerKind.Invoke,
});
return completions.sort((a, b) => (a.label as string).localeCompare(b.label as string));
}
function assertCompletionsEqual(actual: readonly vscode.CompletionItem[], expected: readonly { label: string; insertText?: string }[]) {
assert.strictEqual(actual.length, expected.length, 'Completion counts should be equal');
for (let i = 0; i < actual.length; ++i) {
assert.strictEqual(actual[i].label, expected[i].label, `Completion labels ${i} should be equal`);
if (typeof expected[i].insertText !== 'undefined') {
assert.strictEqual(actual[i].insertText, expected[i].insertText, `Completion insert texts ${i} should be equal`);
}
}
}
suite('Markdown: Path completions', () => {
test('Should not return anything when triggered in empty doc', async () => {
const completions = await getCompletionsAtCursor(workspacePath('new.md'), `${CURSOR}`);
assertCompletionsEqual(completions, []);
});
test('Should return anchor completions', async () => {
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`[](#${CURSOR}`,
``,
`# A b C`,
`# x y Z`,
));
assertCompletionsEqual(completions, [
{ label: '#a-b-c' },
{ label: '#x-y-z' },
]);
});
test('Should not return suggestions for http links', async () => {
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`[](http:${CURSOR}`,
``,
`# http`,
`# http:`,
`# https:`,
));
assertCompletionsEqual(completions, []);
});
test('Should return relative path suggestions', async () => {
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('a.md'), ''),
new InMemoryDocument(workspacePath('b.md'), ''),
new InMemoryDocument(workspacePath('sub/foo.md'), ''),
]);
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`[](${CURSOR}`,
``,
`# A b C`,
), workspace);
assertCompletionsEqual(completions, [
{ label: '#a-b-c' },
{ label: 'a.md' },
{ label: 'b.md' },
{ label: 'sub/' },
]);
});
test('Should return relative path suggestions using ./', async () => {
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('a.md'), ''),
new InMemoryDocument(workspacePath('b.md'), ''),
new InMemoryDocument(workspacePath('sub/foo.md'), ''),
]);
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`[](./${CURSOR}`,
``,
`# A b C`,
), workspace);
assertCompletionsEqual(completions, [
{ label: 'a.md' },
{ label: 'b.md' },
{ label: 'sub/' },
]);
});
test('Should return absolute path suggestions using /', async () => {
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('a.md'), ''),
new InMemoryDocument(workspacePath('b.md'), ''),
new InMemoryDocument(workspacePath('sub/c.md'), ''),
]);
const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines(
`[](/${CURSOR}`,
``,
`# A b C`,
), workspace);
assertCompletionsEqual(completions, [
{ label: 'a.md' },
{ label: 'b.md' },
{ label: 'sub/' },
]);
});
test('Should return anchor suggestions in other file', async () => {
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('b.md'), joinLines(
`# b`,
``,
`[./a](./a)`,
``,
`# header1`,
)),
]);
const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines(
`[](/b.md#${CURSOR}`,
), workspace);
assertCompletionsEqual(completions, [
{ label: '#b' },
{ label: '#header1' },
]);
});
test('Should reference links for current file', async () => {
const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines(
`[][${CURSOR}`,
``,
`[ref-1]: bla`,
`[ref-2]: bla`,
));
assertCompletionsEqual(completions, [
{ label: 'ref-1' },
{ label: 'ref-2' },
]);
});
test('Should complete headers in link definitions', async () => {
const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines(
`# a B c`,
`# x y Z`,
`[ref-1]: ${CURSOR}`,
));
assertCompletionsEqual(completions, [
{ label: '#a-b-c' },
{ label: '#x-y-z' },
{ label: 'new.md' },
]);
});
test('Should complete relative paths in link definitions', async () => {
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('a.md'), ''),
new InMemoryDocument(workspacePath('b.md'), ''),
new InMemoryDocument(workspacePath('sub/c.md'), ''),
]);
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`# a B c`,
`[ref-1]: ${CURSOR}`,
), workspace);
assertCompletionsEqual(completions, [
{ label: '#a-b-c' },
{ label: 'a.md' },
{ label: 'b.md' },
{ label: 'sub/' },
]);
});
test('Should escape spaces in path names', async () => {
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('a.md'), ''),
new InMemoryDocument(workspacePath('b.md'), ''),
new InMemoryDocument(workspacePath('sub/file with space.md'), ''),
]);
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`[](./sub/${CURSOR})`
), workspace);
assertCompletionsEqual(completions, [
{ label: 'file with space.md', insertText: 'file%20with%20space.md' },
]);
});
test('Should support completions on angle bracket path with spaces', async () => {
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('sub with space/a.md'), ''),
new InMemoryDocument(workspacePath('b.md'), ''),
]);
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`[](</sub with space/${CURSOR}`
), workspace);
assertCompletionsEqual(completions, [
{ label: 'a.md', insertText: 'a.md' },
]);
});
test('Should not escape spaces in path names that use angle brackets', async () => {
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('sub/file with space.md'), ''),
]);
{
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`[](<./sub/${CURSOR}`
), workspace);
assertCompletionsEqual(completions, [
{ label: 'file with space.md', insertText: 'file with space.md' },
]);
}
{
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`[](<./sub/${CURSOR}>`
), workspace);
assertCompletionsEqual(completions, [
{ label: 'file with space.md', insertText: 'file with space.md' },
]);
}
});
test('Should complete paths for path with encoded spaces', async () => {
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('a.md'), ''),
new InMemoryDocument(workspacePath('b.md'), ''),
new InMemoryDocument(workspacePath('sub with space/file.md'), ''),
]);
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`[](./sub%20with%20space/${CURSOR})`
), workspace);
assertCompletionsEqual(completions, [
{ label: 'file.md', insertText: 'file.md' },
]);
});
test('Should complete definition path for path with encoded spaces', async () => {
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('a.md'), ''),
new InMemoryDocument(workspacePath('b.md'), ''),
new InMemoryDocument(workspacePath('sub with space/file.md'), ''),
]);
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`[def]: ./sub%20with%20space/${CURSOR}`
), workspace);
assertCompletionsEqual(completions, [
{ label: 'file.md', insertText: 'file.md' },
]);
});
test('Should support definition path with angle brackets', async () => {
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('a.md'), ''),
new InMemoryDocument(workspacePath('b.md'), ''),
new InMemoryDocument(workspacePath('sub with space/file.md'), ''),
]);
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
`[def]: <./${CURSOR}>`
), workspace);
assertCompletionsEqual(completions, [
{ label: 'a.md', insertText: 'a.md' },
{ label: 'b.md', insertText: 'b.md' },
{ label: 'sub with space/', insertText: 'sub with space/' },
]);
});
});

View file

@ -1,635 +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 { MdReferencesProvider, MdVsCodeReferencesProvider } from '../languageFeatures/references';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { DisposableStore } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { IMdWorkspace } from '../workspace';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { joinLines, withStore, workspacePath } from './util';
async function getReferences(store: DisposableStore, doc: InMemoryDocument, pos: vscode.Position, workspace: IMdWorkspace) {
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const computer = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger));
const provider = new MdVsCodeReferencesProvider(computer);
const refs = await provider.provideReferences(doc, pos, { includeDeclaration: true }, noopToken);
return refs.sort((a, b) => {
const pathCompare = a.uri.toString().localeCompare(b.uri.toString());
if (pathCompare !== 0) {
return pathCompare;
}
return a.range.start.compareTo(b.range.start);
});
}
function assertReferencesEqual(actualRefs: readonly vscode.Location[], ...expectedRefs: { uri: vscode.Uri; line: number; startCharacter?: number; endCharacter?: number }[]) {
assert.strictEqual(actualRefs.length, expectedRefs.length, `Reference counts should match`);
for (let i = 0; i < actualRefs.length; ++i) {
const actual = actualRefs[i];
const expected = expectedRefs[i];
assert.strictEqual(actual.uri.toString(), expected.uri.toString(), `Ref '${i}' has expected document`);
assert.strictEqual(actual.range.start.line, expected.line, `Ref '${i}' has expected start line`);
assert.strictEqual(actual.range.end.line, expected.line, `Ref '${i}' has expected end line`);
if (typeof expected.startCharacter !== 'undefined') {
assert.strictEqual(actual.range.start.character, expected.startCharacter, `Ref '${i}' has expected start character`);
}
if (typeof expected.endCharacter !== 'undefined') {
assert.strictEqual(actual.range.end.character, expected.endCharacter, `Ref '${i}' has expected end character`);
}
}
}
suite('Markdown: Find all references', () => {
test('Should not return references when not on header or link', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`# abc`,
``,
`[link 1](#abc)`,
`text`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
{
const refs = await getReferences(store, doc, new vscode.Position(1, 0), workspace);
assert.deepStrictEqual(refs, []);
}
{
const refs = await getReferences(store, doc, new vscode.Position(3, 2), workspace);
assert.deepStrictEqual(refs, []);
}
}));
test('Should find references from header within same file', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# abc`,
``,
`[link 1](#abc)`,
`[not link](#noabc)`,
`[link 2](#abc)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 3), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 2 },
{ uri, line: 4 },
);
}));
test('Should not return references when on link text', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`[ref](#abc)`,
`[ref]: http://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 1), workspace);
assert.deepStrictEqual(refs, []);
}));
test('Should find references using normalized slug', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`# a B c`,
`[simple](#a-b-c)`,
`[start underscore](#_a-b-c)`,
`[different case](#a-B-C)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
{
// Trigger header
const refs = await getReferences(store, doc, new vscode.Position(0, 0), workspace);
assert.deepStrictEqual(refs!.length, 4);
}
{
// Trigger on line 1
const refs = await getReferences(store, doc, new vscode.Position(1, 12), workspace);
assert.deepStrictEqual(refs!.length, 4);
}
{
// Trigger on line 2
const refs = await getReferences(store, doc, new vscode.Position(2, 24), workspace);
assert.deepStrictEqual(refs!.length, 4);
}
{
// Trigger on line 3
const refs = await getReferences(store, doc, new vscode.Position(3, 20), workspace);
assert.deepStrictEqual(refs!.length, 4);
}
}));
test('Should find references from header across files', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const other1Uri = workspacePath('sub', 'other.md');
const other2Uri = workspacePath('zOther2.md');
const doc = new InMemoryDocument(docUri, joinLines(
`# abc`,
``,
`[link 1](#abc)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(other1Uri, joinLines(
`[not link](#abc)`,
`[not link](/doc.md#abz)`,
`[link](/doc.md#abc)`
)),
new InMemoryDocument(other2Uri, joinLines(
`[not link](#abc)`,
`[not link](./doc.md#abz)`,
`[link](./doc.md#abc)`
))
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 3), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 }, // Header definition
{ uri: docUri, line: 2 },
{ uri: other1Uri, line: 2 },
{ uri: other2Uri, line: 2 },
);
}));
test('Should find references from header to link definitions ', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# abc`,
``,
`[bla]: #abc`
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 3), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 }, // Header definition
{ uri, line: 2 },
);
}));
test('Should find header references from link definition', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# A b C`,
`[text][bla]`,
`[bla]: #a-b-c`, // trigger here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(2, 9), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 }, // Header definition
{ uri, line: 2 },
);
}));
test('Should find references from link within same file', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# abc`,
``,
`[link 1](#abc)`,
`[not link](#noabc)`,
`[link 2](#abc)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(2, 10), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 }, // Header definition
{ uri, line: 2 },
{ uri, line: 4 },
);
}));
test('Should find references from link across files', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const other1Uri = workspacePath('sub', 'other.md');
const other2Uri = workspacePath('zOther2.md');
const doc = new InMemoryDocument(docUri, joinLines(
`# abc`,
``,
`[link 1](#abc)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(other1Uri, joinLines(
`[not link](#abc)`,
`[not link](/doc.md#abz)`,
`[with ext](/doc.md#abc)`,
`[without ext](/doc#abc)`
)),
new InMemoryDocument(other2Uri, joinLines(
`[not link](#abc)`,
`[not link](./doc.md#abz)`,
`[link](./doc.md#abc)`
))
]));
const refs = await getReferences(store, doc, new vscode.Position(2, 10), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 }, // Header definition
{ uri: docUri, line: 2 },
{ uri: other1Uri, line: 2 }, // Other with ext
{ uri: other1Uri, line: 3 }, // Other without ext
{ uri: other2Uri, line: 2 }, // Other2
);
}));
test('Should find references without requiring file extensions', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const other1Uri = workspacePath('other.md');
const doc = new InMemoryDocument(docUri, joinLines(
`# a B c`,
``,
`[link 1](#a-b-c)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(other1Uri, joinLines(
`[not link](#a-b-c)`,
`[not link](/doc.md#a-b-z)`,
`[with ext](/doc.md#a-b-c)`,
`[without ext](/doc#a-b-c)`,
`[rel with ext](./doc.md#a-b-c)`,
`[rel without ext](./doc#a-b-c)`
)),
]));
const refs = await getReferences(store, doc, new vscode.Position(2, 10), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 }, // Header definition
{ uri: docUri, line: 2 },
{ uri: other1Uri, line: 2 }, // Other with ext
{ uri: other1Uri, line: 3 }, // Other without ext
{ uri: other1Uri, line: 4 }, // Other relative link with ext
{ uri: other1Uri, line: 5 }, // Other relative link without ext
);
}));
test('Should find references from link across files when triggered on link without file extension', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const other1Uri = workspacePath('sub', 'other.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[with ext](./sub/other#header)`,
`[without ext](./sub/other.md#header)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(other1Uri, joinLines(
`pre`,
`# header`,
`post`
)),
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 23), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 1 },
{ uri: other1Uri, line: 1 }, // Header definition
);
}));
test('Should include header references when triggered on file link', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const otherUri = workspacePath('sub', 'other.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[with ext](./sub/other)`,
`[with ext](./sub/other#header)`,
`[without ext](./sub/other.md#no-such-header)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(otherUri, joinLines(
`pre`,
`# header`,
`post`
)),
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 15), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 1 },
{ uri: docUri, line: 2 },
);
}));
test('Should not include refs from other file to own header', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const otherUri = workspacePath('sub', 'other.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[other](./sub/other)`, // trigger here
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(otherUri, joinLines(
`# header`, // Definition should not be included since we triggered on a file link
`[text](#header)`, // Ref should not be included since it is to own file
)),
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 15), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
);
}));
test('Should find explicit references to own file ', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[bare](doc.md)`, // trigger here
`[rel](./doc.md)`,
`[abs](/doc.md)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 12), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 1 },
{ uri, line: 2 },
);
}));
test('Should support finding references to http uri', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[1](http://example.com)`,
`[no](https://example.com)`,
`[2](http://example.com)`,
`[3]: http://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 13), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 2 },
{ uri, line: 3 },
);
}));
test('Should consider authority, scheme and paths when finding references to http uri', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[1](http://example.com/cat)`,
`[2](http://example.com)`,
`[3](http://example.com/dog)`,
`[4](http://example.com/cat/looong)`,
`[5](http://example.com/cat)`,
`[6](http://other.com/cat)`,
`[7](https://example.com/cat)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 13), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 4 },
);
}));
test('Should support finding references to http uri across files', withStore(async (store) => {
const uri1 = workspacePath('doc.md');
const uri2 = workspacePath('doc2.md');
const doc = new InMemoryDocument(uri1, joinLines(
`[1](http://example.com)`,
`[3]: http://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(uri2, joinLines(
`[other](http://example.com)`
))
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 13), workspace);
assertReferencesEqual(refs!,
{ uri: uri1, line: 0 },
{ uri: uri1, line: 1 },
{ uri: uri2, line: 0 },
);
}));
test('Should support finding references to autolinked http links', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[1](http://example.com)`,
`<http://example.com>`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 13), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 1 },
);
}));
test('Should distinguish between references to file and to header within file', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const other1Uri = workspacePath('sub', 'other.md');
const doc = new InMemoryDocument(docUri, joinLines(
`# abc`,
``,
`[link 1](#abc)`,
));
const otherDoc = new InMemoryDocument(other1Uri, joinLines(
`[link](/doc.md#abc)`,
`[link no text](/doc#abc)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
otherDoc,
]));
{
// Check refs to header fragment
const headerRefs = await getReferences(store, otherDoc, new vscode.Position(0, 16), workspace);
assertReferencesEqual(headerRefs,
{ uri: docUri, line: 0 }, // Header definition
{ uri: docUri, line: 2 },
{ uri: other1Uri, line: 0 },
{ uri: other1Uri, line: 1 },
);
}
{
// Check refs to file itself from link with ext
const fileRefs = await getReferences(store, otherDoc, new vscode.Position(0, 9), workspace);
assertReferencesEqual(fileRefs,
{ uri: other1Uri, line: 0, endCharacter: 14 },
{ uri: other1Uri, line: 1, endCharacter: 19 },
);
}
{
// Check refs to file itself from link without ext
const fileRefs = await getReferences(store, otherDoc, new vscode.Position(1, 17), workspace);
assertReferencesEqual(fileRefs,
{ uri: other1Uri, line: 0 },
{ uri: other1Uri, line: 1 },
);
}
}));
test('Should support finding references to unknown file', withStore(async (store) => {
const uri1 = workspacePath('doc1.md');
const doc1 = new InMemoryDocument(uri1, joinLines(
`![img](/images/more/image.png)`,
``,
`[ref]: /images/more/image.png`,
));
const uri2 = workspacePath('sub', 'doc2.md');
const doc2 = new InMemoryDocument(uri2, joinLines(
`![img](/images/more/image.png)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2]));
const refs = await getReferences(store, doc1, new vscode.Position(0, 10), workspace);
assertReferencesEqual(refs!,
{ uri: uri1, line: 0 },
{ uri: uri1, line: 2 },
{ uri: uri2, line: 0 },
);
}));
suite('Reference links', () => {
test('Should find reference links within file from link', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`, // trigger here
``,
`[abc]: https://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 12), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
);
}));
test('Should find reference links using shorthand', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[ref]`, // trigger 1
``,
`[yes][ref]`, // trigger 2
``,
`[ref]: /Hello.md` // trigger 3
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
{
const refs = await getReferences(store, doc, new vscode.Position(0, 2), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
{ uri: docUri, line: 4 },
);
}
{
const refs = await getReferences(store, doc, new vscode.Position(2, 7), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
{ uri: docUri, line: 4 },
);
}
{
const refs = await getReferences(store, doc, new vscode.Position(4, 2), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
{ uri: docUri, line: 4 },
);
}
}));
test('Should find reference links within file from definition', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com`, // trigger here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(2, 3), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
);
}));
test('Should not find reference links across files', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(workspacePath('other.md'), joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com?bad`
))
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 12), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
);
}));
test('Should not consider checkboxes as reference links', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`- [x]`,
`- [X]`,
`- [ ]`,
``,
`[x]: https://example.com`
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 4), workspace);
assert.strictEqual(refs?.length!, 0);
}));
});
});

View file

@ -1,720 +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 { MdReferencesProvider } from '../languageFeatures/references';
import { MdVsCodeRenameProvider, MdWorkspaceEdit } from '../languageFeatures/rename';
import { githubSlugifier } from '../slugify';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { DisposableStore } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { IMdWorkspace } from '../workspace';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { assertRangeEqual, joinLines, withStore, workspacePath } from './util';
/**
* Get prepare rename info.
*/
function prepareRename(store: DisposableStore, doc: InMemoryDocument, pos: vscode.Position, workspace: IMdWorkspace): Promise<undefined | { readonly range: vscode.Range; readonly placeholder: string }> {
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const referenceComputer = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger));
const renameProvider = store.add(new MdVsCodeRenameProvider(workspace, referenceComputer, githubSlugifier));
return renameProvider.prepareRename(doc, pos, noopToken);
}
/**
* Get all the edits for the rename.
*/
function getRenameEdits(store: DisposableStore, doc: InMemoryDocument, pos: vscode.Position, newName: string, workspace: IMdWorkspace): Promise<MdWorkspaceEdit | undefined> {
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const referencesProvider = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger));
const renameProvider = store.add(new MdVsCodeRenameProvider(workspace, referencesProvider, githubSlugifier));
return renameProvider.provideRenameEditsImpl(doc, pos, newName, noopToken);
}
interface ExpectedTextEdit {
readonly uri: vscode.Uri;
readonly edits: readonly vscode.TextEdit[];
}
interface ExpectedFileRename {
readonly originalUri: vscode.Uri;
readonly newUri: vscode.Uri;
}
function assertEditsEqual(actualEdit: MdWorkspaceEdit, ...expectedEdits: ReadonlyArray<ExpectedTextEdit | ExpectedFileRename>) {
// Check file renames
const expectedFileRenames = expectedEdits.filter(expected => 'originalUri' in expected) as ExpectedFileRename[];
const actualFileRenames = actualEdit.fileRenames ?? [];
assert.strictEqual(actualFileRenames.length, expectedFileRenames.length, `File rename count should match`);
for (let i = 0; i < actualFileRenames.length; ++i) {
const expected = expectedFileRenames[i];
const actual = actualFileRenames[i];
assert.strictEqual(actual.from.toString(), expected.originalUri.toString(), `File rename '${i}' should have expected 'from' resource`);
assert.strictEqual(actual.to.toString(), expected.newUri.toString(), `File rename '${i}' should have expected 'to' resource`);
}
// Check text edits
const actualTextEdits = actualEdit.edit.entries();
const expectedTextEdits = expectedEdits.filter(expected => 'edits' in expected) as ExpectedTextEdit[];
assert.strictEqual(actualTextEdits.length, expectedTextEdits.length, `Reference counts should match`);
for (let i = 0; i < actualTextEdits.length; ++i) {
const expected = expectedTextEdits[i];
const actual = actualTextEdits[i];
if ('edits' in expected) {
assert.strictEqual(actual[0].toString(), expected.uri.toString(), `Ref '${i}' has expected document`);
const actualEditForDoc = actual[1];
const expectedEditsForDoc = expected.edits;
assert.strictEqual(actualEditForDoc.length, expectedEditsForDoc.length, `Edit counts for '${actual[0]}' should match`);
for (let g = 0; g < actualEditForDoc.length; ++g) {
assertRangeEqual(actualEditForDoc[g].range, expectedEditsForDoc[g].range, `Edit '${g}' of '${actual[0]}' has expected expected range. Expected range: ${JSON.stringify(actualEditForDoc[g].range)}. Actual range: ${JSON.stringify(expectedEditsForDoc[g].range)}`);
assert.strictEqual(actualEditForDoc[g].newText, expectedEditsForDoc[g].newText, `Edit '${g}' of '${actual[0]}' has expected edits`);
}
}
}
}
suite('markdown: rename', () => {
setup(async () => {
// the tests make the assumption that link providers are already registered
await vscode.extensions.getExtension('vscode.markdown-language-features')!.activate();
});
test('Rename on header should not include leading #', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# abc`
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(0, 0), workspace);
assertRangeEqual(info!.range, new vscode.Range(0, 2, 0, 5));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 0), "New Header", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 2, 0, 5), 'New Header')
]
});
}));
test('Rename on header should include leading or trailing #s', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`### abc ###`
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(0, 0), workspace);
assertRangeEqual(info!.range, new vscode.Range(0, 4, 0, 7));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 0), "New Header", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 7), 'New Header')
]
});
}));
test('Rename on header should pick up links in doc', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`### A b C`, // rename here
`[text](#a-b-c)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 0), "New Header", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'),
new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'),
]
});
}));
test('Rename on link should use slug for link', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`### A b C`,
`[text](#a-b-c)`, // rename here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(1, 10), "New Header", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'),
new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'),
]
});
}));
test('Rename on link definition should work', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`### A b C`,
`[text](#a-b-c)`,
`[ref]: #a-b-c`// rename here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(2, 10), "New Header", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'),
new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'),
new vscode.TextEdit(new vscode.Range(2, 8, 2, 13), 'new-header'),
]
});
}));
test('Rename on header should pick up links across files', withStore(async (store) => {
const uri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const doc = new InMemoryDocument(uri, joinLines(
`### A b C`, // rename here
`[text](#a-b-c)`,
));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 0), "New Header", new InMemoryMdWorkspace([
doc,
new InMemoryDocument(otherUri, joinLines(
`[text](#a-b-c)`, // Should not find this
`[text](./doc.md#a-b-c)`, // But should find this
`[text](./doc#a-b-c)`, // And this
))
]));
assertEditsEqual(edit!, {
uri: uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'),
new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'),
]
}, {
uri: otherUri, edits: [
new vscode.TextEdit(new vscode.Range(1, 16, 1, 21), 'new-header'),
new vscode.TextEdit(new vscode.Range(2, 13, 2, 18), 'new-header'),
]
});
}));
test('Rename on link should pick up links across files', withStore(async (store) => {
const uri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const doc = new InMemoryDocument(uri, joinLines(
`### A b C`,
`[text](#a-b-c)`, // rename here
));
const edit = await getRenameEdits(store, doc, new vscode.Position(1, 10), "New Header", new InMemoryMdWorkspace([
doc,
new InMemoryDocument(otherUri, joinLines(
`[text](#a-b-c)`, // Should not find this
`[text](./doc.md#a-b-c)`, // But should find this
`[text](./doc#a-b-c)`, // And this
))
]));
assertEditsEqual(edit!, {
uri: uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'),
new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'),
]
}, {
uri: otherUri, edits: [
new vscode.TextEdit(new vscode.Range(1, 16, 1, 21), 'new-header'),
new vscode.TextEdit(new vscode.Range(2, 13, 2, 18), 'new-header'),
]
});
}));
test('Rename on link in other file should pick up all refs', withStore(async (store) => {
const uri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const doc = new InMemoryDocument(uri, joinLines(
`### A b C`,
`[text](#a-b-c)`,
));
const otherDoc = new InMemoryDocument(otherUri, joinLines(
`[text](#a-b-c)`,
`[text](./doc.md#a-b-c)`,
`[text](./doc#a-b-c)`
));
const expectedEdits = [
{
uri: uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'),
new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'),
]
}, {
uri: otherUri, edits: [
new vscode.TextEdit(new vscode.Range(1, 16, 1, 21), 'new-header'),
new vscode.TextEdit(new vscode.Range(2, 13, 2, 18), 'new-header'),
]
}
];
{
// Rename on header with file extension
const edit = await getRenameEdits(store, otherDoc, new vscode.Position(1, 17), "New Header", new InMemoryMdWorkspace([
doc,
otherDoc
]));
assertEditsEqual(edit!, ...expectedEdits);
}
{
// Rename on header without extension
const edit = await getRenameEdits(store, otherDoc, new vscode.Position(2, 15), "New Header", new InMemoryMdWorkspace([
doc,
otherDoc
]));
assertEditsEqual(edit!, ...expectedEdits);
}
}));
test('Rename on reference should rename references and definition', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text][ref]`, // rename here
`[other][ref]`,
``,
`[ref]: https://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 8), "new ref", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 10), 'new ref'),
new vscode.TextEdit(new vscode.Range(1, 8, 1, 11), 'new ref'),
new vscode.TextEdit(new vscode.Range(3, 1, 3, 4), 'new ref'),
]
});
}));
test('Rename on definition should rename references and definitions', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text][ref]`,
`[other][ref]`,
``,
`[ref]: https://example.com`, // rename here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(3, 3), "new ref", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 10), 'new ref'),
new vscode.TextEdit(new vscode.Range(1, 8, 1, 11), 'new ref'),
new vscode.TextEdit(new vscode.Range(3, 1, 3, 4), 'new ref'),
]
});
}));
test('Rename on definition entry should rename header and references', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# a B c`,
`[ref text][ref]`,
`[direct](#a-b-c)`,
`[ref]: #a-b-c`, // rename here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const preparedInfo = await prepareRename(store, doc, new vscode.Position(3, 10), workspace);
assert.strictEqual(preparedInfo!.placeholder, 'a B c');
assertRangeEqual(preparedInfo!.range, new vscode.Range(3, 8, 3, 13));
const edit = await getRenameEdits(store, doc, new vscode.Position(3, 10), "x Y z", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 2, 0, 7), 'x Y z'),
new vscode.TextEdit(new vscode.Range(2, 10, 2, 15), 'x-y-z'),
new vscode.TextEdit(new vscode.Range(3, 8, 3, 13), 'x-y-z'),
]
});
}));
test('Rename should not be supported on link text', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# Header`,
`[text](#header)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
await assert.rejects(prepareRename(store, doc, new vscode.Position(1, 2), workspace));
}));
test('Path rename should use file path as range', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](./doc.md)`,
`[ref]: ./doc.md`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(0, 10), workspace);
assert.strictEqual(info!.placeholder, './doc.md');
assertRangeEqual(info!.range, new vscode.Range(0, 7, 0, 15));
}));
test('Path rename\'s range should excludes fragment', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](./doc.md#some-header)`,
`[ref]: ./doc.md#some-header`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(0, 10), workspace);
assert.strictEqual(info!.placeholder, './doc.md');
assertRangeEqual(info!.range, new vscode.Range(0, 7, 0, 15));
}));
test('Path rename should update file and all refs', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](./doc.md)`,
`[ref]: ./doc.md`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 10), './sub/newDoc.md', workspace);
assertEditsEqual(edit!, {
originalUri: uri,
newUri: workspacePath('sub', 'newDoc.md'),
}, {
uri: uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 15), './sub/newDoc.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 15), './sub/newDoc.md'),
]
});
}));
test('Path rename using absolute file path should anchor to workspace root', withStore(async (store) => {
const uri = workspacePath('sub', 'doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](/sub/doc.md)`,
`[ref]: /sub/doc.md`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 10), '/newSub/newDoc.md', workspace);
assertEditsEqual(edit!, {
originalUri: uri,
newUri: workspacePath('newSub', 'newDoc.md'),
}, {
uri: uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 18), '/newSub/newDoc.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 18), '/newSub/newDoc.md'),
]
});
}));
test('Path rename should use un-encoded paths as placeholder', withStore(async (store) => {
const uri = workspacePath('sub', 'doc with spaces.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](/sub/doc%20with%20spaces.md)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(0, 10), workspace);
assert.strictEqual(info!.placeholder, '/sub/doc with spaces.md');
}));
test('Path rename should encode paths', withStore(async (store) => {
const uri = workspacePath('sub', 'doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](/sub/doc.md)`,
`[ref]: /sub/doc.md`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 10), '/NEW sub/new DOC.md', workspace);
assertEditsEqual(edit!, {
originalUri: uri,
newUri: workspacePath('NEW sub', 'new DOC.md'),
}, {
uri: uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 18), '/NEW%20sub/new%20DOC.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 18), '/NEW%20sub/new%20DOC.md'),
]
});
}));
test('Path rename should work with unknown files', withStore(async (store) => {
const uri1 = workspacePath('doc1.md');
const doc1 = new InMemoryDocument(uri1, joinLines(
`![img](/images/more/image.png)`,
``,
`[ref]: /images/more/image.png`,
));
const uri2 = workspacePath('sub', 'doc2.md');
const doc2 = new InMemoryDocument(uri2, joinLines(
`![img](/images/more/image.png)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc1,
doc2
]));
const edit = await getRenameEdits(store, doc1, new vscode.Position(0, 10), '/img/test/new.png', workspace);
assertEditsEqual(edit!,
// Should not have file edits since the files don't exist here
{
uri: uri1, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 29), '/img/test/new.png'),
new vscode.TextEdit(new vscode.Range(2, 7, 2, 29), '/img/test/new.png'),
]
},
{
uri: uri2, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 29), '/img/test/new.png'),
]
});
}));
test('Path rename should use .md extension on extension-less link', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](/doc#header)`,
`[ref]: /doc#other`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 10), '/new File', workspace);
assertEditsEqual(edit!, {
originalUri: uri,
newUri: workspacePath('new File.md'), // Rename on disk should use file extension
}, {
uri: uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 11), '/new%20File'), // Links should continue to use extension-less paths
new vscode.TextEdit(new vscode.Range(1, 7, 1, 11), '/new%20File'),
]
});
}));
// TODO: fails on windows
test.skip('Path rename should use correctly resolved paths across files', withStore(async (store) => {
const uri1 = workspacePath('sub', 'doc.md');
const doc1 = new InMemoryDocument(uri1, joinLines(
`[text](./doc.md)`,
`[ref]: ./doc.md`,
));
const uri2 = workspacePath('doc2.md');
const doc2 = new InMemoryDocument(uri2, joinLines(
`[text](./sub/doc.md)`,
`[ref]: ./sub/doc.md`,
));
const uri3 = workspacePath('sub2', 'doc3.md');
const doc3 = new InMemoryDocument(uri3, joinLines(
`[text](../sub/doc.md)`,
`[ref]: ../sub/doc.md`,
));
const uri4 = workspacePath('sub2', 'doc4.md');
const doc4 = new InMemoryDocument(uri4, joinLines(
`[text](/sub/doc.md)`,
`[ref]: /sub/doc.md`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc1, doc2, doc3, doc4,
]));
const edit = await getRenameEdits(store, doc1, new vscode.Position(0, 10), './new/new-doc.md', workspace);
assertEditsEqual(edit!, {
originalUri: uri1,
newUri: workspacePath('sub', 'new', 'new-doc.md'),
}, {
uri: uri1, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 15), './new/new-doc.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 15), './new/new-doc.md'),
]
}, {
uri: uri2, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 19), './sub/new/new-doc.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 19), './sub/new/new-doc.md'),
]
}, {
uri: uri3, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 20), '../sub/new/new-doc.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 20), '../sub/new/new-doc.md'),
]
}, {
uri: uri4, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 18), '/sub/new/new-doc.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 18), '/sub/new/new-doc.md'),
]
});
}));
test('Path rename should resolve on links without prefix', withStore(async (store) => {
const uri1 = workspacePath('sub', 'doc.md');
const doc1 = new InMemoryDocument(uri1, joinLines(
`![text](sub2/doc3.md)`,
));
const uri2 = workspacePath('doc2.md');
const doc2 = new InMemoryDocument(uri2, joinLines(
`![text](sub/sub2/doc3.md)`,
));
const uri3 = workspacePath('sub', 'sub2', 'doc3.md');
const doc3 = new InMemoryDocument(uri3, joinLines());
const workspace = store.add(new InMemoryMdWorkspace([
doc1, doc2, doc3
]));
const edit = await getRenameEdits(store, doc1, new vscode.Position(0, 10), 'sub2/cat.md', workspace);
assertEditsEqual(edit!, {
originalUri: workspacePath('sub', 'sub2', 'doc3.md'),
newUri: workspacePath('sub', 'sub2', 'cat.md'),
}, {
uri: uri1, edits: [new vscode.TextEdit(new vscode.Range(0, 8, 0, 20), 'sub2/cat.md')]
}, {
uri: uri2, edits: [new vscode.TextEdit(new vscode.Range(0, 8, 0, 24), 'sub/sub2/cat.md')]
});
}));
test('Rename on link should use header text as placeholder', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`### a B c ###`,
`[text](#a-b-c)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(1, 10), workspace);
assert.strictEqual(info!.placeholder, 'a B c');
assertRangeEqual(info!.range, new vscode.Range(1, 8, 1, 13));
}));
test('Rename on http uri should work', withStore(async (store) => {
const uri1 = workspacePath('doc.md');
const uri2 = workspacePath('doc2.md');
const doc = new InMemoryDocument(uri1, joinLines(
`[1](http://example.com)`,
`[2]: http://example.com`,
`<http://example.com>`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(uri2, joinLines(
`[4](http://example.com)`
))
]));
const edit = await getRenameEdits(store, doc, new vscode.Position(1, 10), "https://example.com/sub", workspace);
assertEditsEqual(edit!, {
uri: uri1, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 22), 'https://example.com/sub'),
new vscode.TextEdit(new vscode.Range(1, 5, 1, 23), 'https://example.com/sub'),
new vscode.TextEdit(new vscode.Range(2, 1, 2, 19), 'https://example.com/sub'),
]
}, {
uri: uri2, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 22), 'https://example.com/sub'),
]
});
}));
test('Rename on definition path should update all references to path', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[ref text][ref]`,
`[direct](/file)`,
`[ref]: /file`, // rename here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const preparedInfo = await prepareRename(store, doc, new vscode.Position(2, 10), workspace);
assert.strictEqual(preparedInfo!.placeholder, '/file');
assertRangeEqual(preparedInfo!.range, new vscode.Range(2, 7, 2, 12));
const edit = await getRenameEdits(store, doc, new vscode.Position(2, 10), "/newFile", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(1, 9, 1, 14), '/newFile'),
new vscode.TextEdit(new vscode.Range(2, 7, 2, 12), '/newFile'),
]
});
}));
test('Rename on definition path where file exists should also update file', withStore(async (store) => {
const uri1 = workspacePath('doc.md');
const doc1 = new InMemoryDocument(uri1, joinLines(
`[ref text][ref]`,
`[direct](/doc2)`,
`[ref]: /doc2`, // rename here
));
const uri2 = workspacePath('doc2.md');
const doc2 = new InMemoryDocument(uri2, joinLines());
const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2]));
const preparedInfo = await prepareRename(store, doc1, new vscode.Position(2, 10), workspace);
assert.strictEqual(preparedInfo!.placeholder, '/doc2');
assertRangeEqual(preparedInfo!.range, new vscode.Range(2, 7, 2, 12));
const edit = await getRenameEdits(store, doc1, new vscode.Position(2, 10), "/new-doc", workspace);
assertEditsEqual(edit!, {
uri: uri1, edits: [
new vscode.TextEdit(new vscode.Range(1, 9, 1, 14), '/new-doc'),
new vscode.TextEdit(new vscode.Range(2, 7, 2, 12), '/new-doc'),
]
}, {
originalUri: uri2,
newUri: workspacePath('new-doc.md')
});
}));
test('Rename on definition path header should update all references to header', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[ref text][ref]`,
`[direct](/file#header)`,
`[ref]: /file#header`, // rename here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const preparedInfo = await prepareRename(store, doc, new vscode.Position(2, 16), workspace);
assert.strictEqual(preparedInfo!.placeholder, 'header');
assertRangeEqual(preparedInfo!.range, new vscode.Range(2, 13, 2, 19));
const edit = await getRenameEdits(store, doc, new vscode.Position(2, 16), "New Header", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(1, 15, 1, 21), 'new-header'),
new vscode.TextEdit(new vscode.Range(2, 13, 2, 19), 'new-header'),
]
});
}));
});

View file

@ -6,28 +6,11 @@ import * as assert from 'assert';
import * as os from 'os';
import * as vscode from 'vscode';
import { DisposableStore } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
export const joinLines = (...args: string[]) =>
args.join(os.platform() === 'win32' ? '\r\n' : '\n');
export const CURSOR = '$$CURSOR$$';
export function getCursorPositions(contents: string, doc: InMemoryDocument): vscode.Position[] {
const positions: vscode.Position[] = [];
let index = 0;
let wordLength = 0;
while (index !== -1) {
index = contents.indexOf(CURSOR, index + wordLength);
if (index !== -1) {
positions.push(doc.positionAt(index));
}
wordLength = CURSOR.length;
}
return positions;
}
export function workspacePath(...segments: string[]): vscode.Uri {
return vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, ...segments);
}

View file

@ -6,7 +6,6 @@
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.textEditorDrop.d.ts",
"../../src/vscode-dts/vscode.proposed.documentPaste.d.ts"
]
}

View file

@ -242,6 +242,11 @@ vscode-languageserver-types@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:
version "3.17.2"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz#b2c2e7de405ad3d73a883e91989b850170ffc4f2"
integrity sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==
vscode-nls@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840"

View file

@ -13,7 +13,7 @@ const fs = require('fs');
const merge = require('merge-options');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { NLSBundlePlugin } = require('vscode-nls-dev/lib/webpack-bundler');
const { DefinePlugin } = require('webpack');
const { DefinePlugin, optimize } = require('webpack');
function withNodeDefaults(/**@type WebpackConfig*/extConfig) {
/** @type WebpackConfig */
@ -145,6 +145,9 @@ function withBrowserDefaults(/**@type WebpackConfig*/extConfig, /** @type Additi
}
const browserPlugins = [
new optimize.LimitChunkCountPlugin({
maxChunks: 1
}),
new CopyWebpackPlugin({
patterns: [
{ from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true }

View file

@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.70.0",
"distro": "1a629baefa2ce65ed9d03176536e957c80bf6703",
"distro": "1a72c46622967eab6ea48516a2153c55d7e18e53",
"author": {
"name": "Microsoft Corporation"
},
@ -85,12 +85,12 @@
"vscode-proxy-agent": "^0.12.0",
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "7.0.1",
"xterm": "4.20.0-beta.12",
"xterm": "4.20.0-beta.13",
"xterm-addon-search": "0.10.0-beta.2",
"xterm-addon-serialize": "0.8.0-beta.2",
"xterm-addon-unicode11": "0.4.0-beta.3",
"xterm-addon-webgl": "0.13.0-beta.6",
"xterm-headless": "4.20.0-beta.12",
"xterm-addon-webgl": "0.13.0-beta.7",
"xterm-headless": "4.20.0-beta.13",
"yauzl": "^2.9.2",
"yazl": "^2.4.3"
},

View file

@ -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.12",
"xterm": "4.20.0-beta.13",
"xterm-addon-search": "0.10.0-beta.2",
"xterm-addon-serialize": "0.8.0-beta.2",
"xterm-addon-unicode11": "0.4.0-beta.3",
"xterm-addon-webgl": "0.13.0-beta.6",
"xterm-headless": "4.20.0-beta.12",
"xterm-addon-webgl": "0.13.0-beta.7",
"xterm-headless": "4.20.0-beta.13",
"yauzl": "^2.9.2",
"yazl": "^2.4.3"
},

View file

@ -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.12",
"xterm": "4.20.0-beta.13",
"xterm-addon-search": "0.10.0-beta.2",
"xterm-addon-unicode11": "0.4.0-beta.3",
"xterm-addon-webgl": "0.13.0-beta.6"
"xterm-addon-webgl": "0.13.0-beta.7"
}
}

View file

@ -78,12 +78,12 @@ xterm-addon-unicode11@0.4.0-beta.3:
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa"
integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q==
xterm-addon-webgl@0.13.0-beta.6:
version "0.13.0-beta.6"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.6.tgz#44eceb1e15a711159bdf1c2a779422aa11becabc"
integrity sha512-83bo12rqYU04agC6rn+drEui8tarN5Ev66sdu86aGzxM+Ylr8wFqIb/Px/caX9qTqO79TN80ID7EC5P5QyL8XA==
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@4.20.0-beta.12:
version "4.20.0-beta.12"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.12.tgz#02751473b307d6795e6f47abf6798993128a84b7"
integrity sha512-6bqZshNOJsghzQ5f52JIPrZL8MPGW+caQkeQ3aoAPleGvZz775LDQAQH2KzdR9XxOJI5wnJcxx+dk7IjulCsnw==
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==

View file

@ -803,20 +803,20 @@ xterm-addon-unicode11@0.4.0-beta.3:
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa"
integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q==
xterm-addon-webgl@0.13.0-beta.6:
version "0.13.0-beta.6"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.6.tgz#44eceb1e15a711159bdf1c2a779422aa11becabc"
integrity sha512-83bo12rqYU04agC6rn+drEui8tarN5Ev66sdu86aGzxM+Ylr8wFqIb/Px/caX9qTqO79TN80ID7EC5P5QyL8XA==
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-headless@4.20.0-beta.12:
version "4.20.0-beta.12"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.12.tgz#a8f14127212ef15b1d4e726014daf422d29d06ba"
integrity sha512-MtxrRy1qm/SQl5oTClK30rzip/WELzkGQ837CiOlGLiktj5yA1gK7SA7T532fIPPa9czJ8jBuONvZQJL3M000A==
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@4.20.0-beta.12:
version "4.20.0-beta.12"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.12.tgz#02751473b307d6795e6f47abf6798993128a84b7"
integrity sha512-6bqZshNOJsghzQ5f52JIPrZL8MPGW+caQkeQ3aoAPleGvZz775LDQAQH2KzdR9XxOJI5wnJcxx+dk7IjulCsnw==
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==
yallist@^4.0.0:
version "4.0.0"

View file

@ -153,9 +153,6 @@ function configureCommandlineSwitchesSync(cliArgs) {
// alias from us for --disable-gpu
'disable-hardware-acceleration',
// provided by Electron
'disable-color-correct-rendering',
// override for the color profile to use
'force-color-profile'
];
@ -247,9 +244,7 @@ function readArgvConfigSync() {
// Fallback to default
if (!argvConfig) {
argvConfig = {
'disable-color-correct-rendering': true // Force pre-Chrome-60 color profile handling (for https://github.com/microsoft/vscode/issues/51791)
};
argvConfig = {};
}
return argvConfig;
@ -279,11 +274,7 @@ function createDefaultArgvConfigSync(argvConfigPath) {
'{',
' // Use software rendering instead of hardware accelerated rendering.',
' // This can help in cases where you see rendering issues in VS Code.',
' // "disable-hardware-acceleration": true,',
'',
' // Enabled by default by VS Code to resolve color issues in the renderer',
' // See https://github.com/microsoft/vscode/issues/51791 for details',
' "disable-color-correct-rendering": true',
' // "disable-hardware-acceleration": true',
'}'
];

View file

@ -1734,50 +1734,87 @@ export function computeClippingRect(elementOrRect: HTMLElement | DOMRectReadOnly
return { top, right, bottom, left };
}
interface DomNodeAttributes {
role?: string;
ariaHidden?: boolean;
style?: StyleAttributes;
}
type HTMLElementAttributeKeys<T> = Partial<{ [K in keyof T]: T[K] extends Function ? never : T[K] extends object ? HTMLElementAttributeKeys<T[K]> : T[K] }>;
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 readonly any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
type HHTMLElementTagNameMap = HTMLElementTagNameMap & { '': HTMLDivElement };
interface StyleAttributes {
height?: number | string;
width?: number | string;
}
type TagToElement<T> = T extends `${infer TStart}#${string}`
? TStart extends keyof HHTMLElementTagNameMap
? HHTMLElementTagNameMap[TStart]
: HTMLElement
: T extends `${infer TStart}.${string}`
? TStart extends keyof HHTMLElementTagNameMap
? HHTMLElementTagNameMap[TStart]
: HTMLElement
: T extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[T]
: HTMLElement;
//<div role="presentation" aria-hidden="true" class="scroll-decoration"></div>
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, TId extends string>(
tag: TTag,
attributes: { $: TId } & DomNodeAttributes
): Record<TId | 'root', TagToElement<TTag>>;
export function h<TTag extends string>(tag: TTag, attributes: DomNodeAttributes): Record<'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, TId extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>(
tag: TTag,
attributes: { $: TId } & DomNodeAttributes,
children: T
): (ArrayToObj<T> & Record<TId, TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h(tag: string, ...args: [] | [attributes: { $: string } & DomNodeAttributes | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
let attributes: { $?: string } & DomNodeAttributes;
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;
if (Array.isArray(args[0])) {
@ -1788,14 +1825,29 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & DomNod
children = args[1];
}
const [tagName, className] = tag.split('.');
const match = H_REGEX.exec(tag);
if (!match || !match.groups) {
throw new Error('Bad use of h');
}
const tagName = match.groups['tag'] || 'div';
const el = document.createElement(tagName);
if (className) {
el.className = className;
if (match.groups['id']) {
el.id = match.groups['id'];
}
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) {
@ -1810,10 +1862,6 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & DomNod
}
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(
@ -1834,24 +1882,3 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & DomNod
function camelCaseToHyphenCase(str: string) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
type RemoveHTMLElement<T> = T extends HTMLElement ? never : T;
type ArrayToObj<T extends any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type HTMLElementsByTagName = {
div: HTMLDivElement;
span: HTMLSpanElement;
a: HTMLAnchorElement;
};
type TagToElement<T> = T extends `${infer TStart}.${string}`
? TStart extends keyof HTMLElementsByTagName
? HTMLElementsByTagName[TStart]
: HTMLElement
: T extends keyof HTMLElementsByTagName
? HTMLElementsByTagName[T]
: HTMLElement;

View file

@ -8,7 +8,7 @@ import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { DisposableStore } from 'vs/base/common/lifecycle';
export interface IContentActionHandler {
callback: (content: string, event?: IMouseEvent) => void;
callback: (content: string, event: IMouseEvent) => void;
readonly disposables: DisposableStore;
}

View file

@ -23,6 +23,7 @@ export interface IBaseActionViewItemOptions {
draggable?: boolean;
isMenu?: boolean;
useEventAsContext?: boolean;
hoverDelegate?: IHoverDelegate;
}
export class BaseActionViewItem extends Disposable implements IActionViewItem {
@ -32,6 +33,8 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem {
_context: unknown;
readonly _action: IAction;
private customHover?: ICustomHover;
get action() {
return this._action;
}
@ -210,8 +213,27 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem {
// implement in subclass
}
protected getTooltip(): string | undefined {
return this.getAction().tooltip;
}
protected updateTooltip(): void {
// implement in subclass
if (!this.element) {
return;
}
const title = this.getTooltip() ?? '';
this.element.setAttribute('aria-label', title);
if (!this.options.hoverDelegate) {
this.element.title = title;
} else {
this.element.title = '';
if (!this.customHover) {
this.customHover = setupCustomHover(this.options.hoverDelegate, this.element, title);
this._store.add(this.customHover);
} else {
this.customHover.update(title);
}
}
}
protected updateClass(): void {
@ -236,7 +258,6 @@ export interface IActionViewItemOptions extends IBaseActionViewItemOptions {
icon?: boolean;
label?: boolean;
keybinding?: string | null;
hoverDelegate?: IHoverDelegate;
}
export class ActionViewItem extends BaseActionViewItem {
@ -245,7 +266,6 @@ export class ActionViewItem extends BaseActionViewItem {
protected override options: IActionViewItemOptions;
private cssClass?: string;
private customHover?: ICustomHover;
constructor(context: unknown, action: IAction, options: IActionViewItemOptions = {}) {
super(context, action, options);
@ -317,7 +337,7 @@ export class ActionViewItem extends BaseActionViewItem {
}
}
override updateTooltip(): void {
override getTooltip() {
let title: string | null = null;
if (this.getAction().tooltip) {
@ -330,24 +350,7 @@ export class ActionViewItem extends BaseActionViewItem {
title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding);
}
}
this._applyUpdateTooltip(title);
}
protected _applyUpdateTooltip(title: string | undefined | null): void {
if (title && this.label) {
this.label.setAttribute('aria-label', title);
if (!this.options.hoverDelegate) {
this.label.title = title;
} else {
this.label.title = '';
if (!this.customHover) {
this.customHover = setupCustomHover(this.options.hoverDelegate, this.label, title);
this._store.add(this.customHover);
} else {
this.customHover.update(title);
}
}
}
return title ?? undefined;
}
override updateClass(): void {

View file

@ -46,8 +46,10 @@
outline-offset: -1px !important;
}
.monaco-button-dropdown.disabled .monaco-button-dropdown-separator {
opacity: 0.4;
.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 !important;
}
.monaco-button-dropdown .monaco-button-dropdown-separator {

View file

@ -28,6 +28,7 @@ export interface IButtonStyles {
buttonBackground?: Color;
buttonHoverBackground?: Color;
buttonForeground?: Color;
buttonSeparator?: Color;
buttonSecondaryBackground?: Color;
buttonSecondaryHoverBackground?: Color;
buttonSecondaryForeground?: Color;
@ -37,6 +38,7 @@ export interface IButtonStyles {
const defaultOptions: IButtonStyles = {
buttonBackground: Color.fromHex('#0E639C'),
buttonHoverBackground: Color.fromHex('#006BB3'),
buttonSeparator: Color.white,
buttonForeground: Color.white
};
@ -315,7 +317,7 @@ export class ButtonWithDropdown extends Disposable implements IButton {
// Separator
this.separatorContainer.style.backgroundColor = styles.buttonBackground?.toString() ?? '';
this.separator.style.backgroundColor = styles.buttonForeground?.toString() ?? '';
this.separator.style.backgroundColor = styles.buttonSeparator?.toString() ?? '';
}
focus(): void {
@ -339,12 +341,10 @@ export class ButtonWithDescription extends Button implements IButtonWithDescript
this._labelElement = document.createElement('div');
this._labelElement.classList.add('monaco-button-label');
this._labelElement.tabIndex = -1;
this._element.appendChild(this._labelElement);
this._descriptionElement = document.createElement('div');
this._descriptionElement.classList.add('monaco-button-description');
this._descriptionElement.tabIndex = -1;
this._element.appendChild(this._descriptionElement);
}

View file

@ -5,7 +5,6 @@
.context-view {
position: absolute;
z-index: 2500;
}
.context-view.fixed {
@ -13,6 +12,5 @@
font-family: inherit;
font-size: 13px;
position: fixed;
z-index: 2500;
color: inherit;
}

View file

@ -206,7 +206,7 @@ export class ContextView extends Disposable {
this.view.className = 'context-view';
this.view.style.top = '0px';
this.view.style.left = '0px';
this.view.style.zIndex = '2500';
this.view.style.zIndex = '2575';
this.view.style.position = this.useFixedPosition ? 'fixed' : 'absolute';
DOM.show(this.view);

View file

@ -6,7 +6,7 @@
import * as dom from 'vs/base/browser/dom';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { IToggleStyles } from 'vs/base/browser/ui/toggle/toggle';
import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { CaseSensitiveToggle, RegexToggle, WholeWordsToggle } from 'vs/base/browser/ui/findinput/findInputToggles';
import { HistoryInputBox, IInputBoxStyles, IInputValidator, IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox';
@ -31,6 +31,7 @@ export interface IFindInputOptions extends IFindInputStyles {
readonly appendWholeWordsLabel?: string;
readonly appendRegexLabel?: string;
readonly history?: string[];
readonly additionalToggles?: Toggle[];
readonly showHistoryHint?: () => boolean;
}
@ -74,6 +75,7 @@ export class FindInput extends Widget {
protected regex: RegexToggle;
protected wholeWords: WholeWordsToggle;
protected caseSensitive: CaseSensitiveToggle;
protected additionalToggles: Toggle[] = [];
public domNode: HTMLElement;
public inputBox: HistoryInputBox;
@ -209,10 +211,6 @@ export class FindInput extends Widget {
this._onCaseSensitiveKeyDown.fire(e);
}));
if (this._showOptionButtons) {
this.inputBox.paddingRight = this.caseSensitive.width() + this.wholeWords.width() + this.regex.width();
}
// Arrow-Key support to navigate between options
const indexes = [this.caseSensitive.domNode, this.wholeWords.domNode, this.regex.domNode];
this.onkeydown(this.domNode, (event: IKeyboardEvent) => {
@ -250,6 +248,34 @@ export class FindInput extends Widget {
this.controls.appendChild(this.wholeWords.domNode);
this.controls.appendChild(this.regex.domNode);
if (!this._showOptionButtons) {
this.caseSensitive.domNode.style.display = 'none';
this.wholeWords.domNode.style.display = 'none';
this.regex.domNode.style.display = 'none';
}
for (const toggle of options?.additionalToggles ?? []) {
this._register(toggle);
this.controls.appendChild(toggle.domNode);
this._register(toggle.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
}));
this.additionalToggles.push(toggle);
}
if (this.additionalToggles.length > 0) {
this.controls.style.display = 'block';
}
this.inputBox.paddingRight =
(this._showOptionButtons ? this.caseSensitive.width() + this.wholeWords.width() + this.regex.width() : 0)
+ this.additionalToggles.reduce((r, t) => r + t.width(), 0);
this.domNode.appendChild(this.controls);
parent?.appendChild(this.domNode);
@ -282,6 +308,10 @@ export class FindInput extends Widget {
this.regex.enable();
this.wholeWords.enable();
this.caseSensitive.enable();
for (const toggle of this.additionalToggles) {
toggle.enable();
}
}
public disable(): void {
@ -290,6 +320,10 @@ export class FindInput extends Widget {
this.regex.disable();
this.wholeWords.disable();
this.caseSensitive.disable();
for (const toggle of this.additionalToggles) {
toggle.disable();
}
}
public setFocusInputOnOptionClick(value: boolean): void {
@ -356,6 +390,10 @@ export class FindInput extends Widget {
this.wholeWords.style(toggleStyles);
this.caseSensitive.style(toggleStyles);
for (const toggle of this.additionalToggles) {
toggle.style(toggleStyles);
}
const inputBoxStyles: IInputBoxStyles = {
inputBackground: this.inputBackground,
inputForeground: this.inputForeground,

View file

@ -65,72 +65,7 @@
z-index: 1000;
}
/* Type filter */
.monaco-list-type-filter {
display: flex;
align-items: center;
position: absolute;
border-radius: 2px;
padding: 0px 3px;
max-width: calc(100% - 10px);
text-overflow: ellipsis;
overflow: hidden;
text-align: right;
box-sizing: border-box;
cursor: all-scroll;
font-size: 13px;
line-height: 18px;
height: 20px;
z-index: 1;
top: 4px;
}
.monaco-list-type-filter.dragging {
transition: top 0.2s, left 0.2s;
}
.monaco-list-type-filter.ne {
right: 4px;
}
.monaco-list-type-filter.nw {
left: 4px;
}
.monaco-list-type-filter > .controls {
display: flex;
align-items: center;
box-sizing: border-box;
transition: width 0.2s;
width: 0;
}
.monaco-list-type-filter.dragging > .controls,
.monaco-list-type-filter:hover > .controls {
width: 36px;
}
.monaco-list-type-filter > .controls > * {
border: none;
box-sizing: border-box;
-webkit-appearance: none;
-moz-appearance: none;
background: none;
width: 16px;
height: 16px;
flex-shrink: 0;
margin: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.monaco-list-type-filter > .controls > .filter {
margin-left: 4px;
}
/* Filter */
.monaco-list-type-filter-message {
position: absolute;
@ -149,13 +84,3 @@
.monaco-list-type-filter-message:empty {
display: none;
}
/* Electron */
.monaco-list-type-filter {
cursor: grab;
}
.monaco-list-type-filter.dragging {
cursor: grabbing;
}

View file

@ -12,7 +12,7 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { IThemable } from 'vs/base/common/styler';
import 'vs/css!./list';
import { IListContextMenuEvent, IListEvent, IListMouseEvent, IListRenderer, IListVirtualDelegate } from './list';
import { IListAccessibilityProvider, IListOptions, IListOptionsUpdate, IListStyles, List } from './listWidget';
import { IListAccessibilityProvider, IListOptions, IListOptionsUpdate, IListStyles, List, TypeNavigationMode } from './listWidget';
export interface IPagedRenderer<TElement, TTemplateData> extends IListRenderer<TElement, TTemplateData> {
renderPlaceholder(index: number, templateData: TTemplateData): void;
@ -95,8 +95,8 @@ class PagedAccessibilityProvider<T> implements IListAccessibilityProvider<number
}
export interface IPagedListOptions<T> {
readonly enableKeyboardNavigation?: boolean;
readonly automaticKeyboardNavigation?: boolean;
readonly typeNavigationEnabled?: boolean;
readonly typeNavigationMode?: TypeNavigationMode;
readonly ariaLabel?: string;
readonly keyboardSupport?: boolean;
readonly multipleSelectionSupport?: boolean;
@ -282,8 +282,8 @@ export class PagedList<T> implements IThemable, IDisposable {
this.list.layout(height, width);
}
toggleKeyboardNavigation(): void {
this.list.toggleKeyboardNavigation();
triggerTypeNavigation(): void {
this.list.triggerTypeNavigation();
}
reveal(index: number, relativeTop?: number): void {

View file

@ -9,6 +9,7 @@ import { DomEmitter, stopEvent } from 'vs/base/browser/event';
import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Gesture } from 'vs/base/browser/touch';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput';
import { CombinedSpliceable } from 'vs/base/browser/ui/list/splice';
import { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
import { binarySearch, firstOrDefault, range } from 'vs/base/common/arrays';
@ -384,7 +385,12 @@ class KeyboardController<T> implements IDisposable {
}
}
enum TypeLabelControllerState {
export enum TypeNavigationMode {
Automatic,
Trigger
}
enum TypeNavigationControllerState {
Idle,
Typing
}
@ -402,12 +408,12 @@ export const DefaultKeyboardNavigationDelegate = new class implements IKeyboardN
}
};
class TypeLabelController<T> implements IDisposable {
class TypeNavigationController<T> implements IDisposable {
private enabled = false;
private state: TypeLabelControllerState = TypeLabelControllerState.Idle;
private state: TypeNavigationControllerState = TypeNavigationControllerState.Idle;
private automaticKeyboardNavigation = true;
private mode = TypeNavigationMode.Automatic;
private triggered = false;
private previouslyFocused = -1;
@ -424,20 +430,16 @@ class TypeLabelController<T> implements IDisposable {
}
updateOptions(options: IListOptions<T>): void {
const enableKeyboardNavigation = typeof options.enableKeyboardNavigation === 'undefined' ? true : !!options.enableKeyboardNavigation;
if (enableKeyboardNavigation) {
if (options.typeNavigationEnabled ?? true) {
this.enable();
} else {
this.disable();
}
if (typeof options.automaticKeyboardNavigation !== 'undefined') {
this.automaticKeyboardNavigation = options.automaticKeyboardNavigation;
}
this.mode = options.typeNavigationMode ?? TypeNavigationMode.Automatic;
}
toggle(): void {
trigger(): void {
this.triggered = !this.triggered;
}
@ -448,10 +450,10 @@ class TypeLabelController<T> implements IDisposable {
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.automaticKeyboardNavigation || this.triggered)
.filter(() => this.mode === TypeNavigationMode.Automatic || this.triggered)
.map(event => new StandardKeyboardEvent(event))
.filter(e => this.delegate.mightProducePrintableCharacter(e))
.forEach(e => e.preventDefault())
.forEach(e => { e.preventDefault(); e.stopPropagation(); })
.map(event => event.browserEvent.key)
.event;
@ -490,15 +492,15 @@ class TypeLabelController<T> implements IDisposable {
private onInput(word: string | null): void {
if (!word) {
this.state = TypeLabelControllerState.Idle;
this.state = TypeNavigationControllerState.Idle;
this.triggered = false;
return;
}
const focus = this.list.getFocus();
const start = focus.length > 0 ? focus[0] : 0;
const delta = this.state === TypeLabelControllerState.Idle ? 1 : 0;
this.state = TypeLabelControllerState.Typing;
const delta = this.state === TypeNavigationControllerState.Idle ? 1 : 0;
this.state = TypeNavigationControllerState.Typing;
for (let i = 0; i < this.list.length; i++) {
const index = (start + i + delta) % this.list.length;
@ -895,22 +897,6 @@ export class DefaultStyleController implements IStyleController {
`);
}
if (styles.listFilterWidgetBackground) {
content.push(`.monaco-list-type-filter { background-color: ${styles.listFilterWidgetBackground} }`);
}
if (styles.listFilterWidgetOutline) {
content.push(`.monaco-list-type-filter { border: 1px solid ${styles.listFilterWidgetOutline}; }`);
}
if (styles.listFilterWidgetNoMatchesOutline) {
content.push(`.monaco-list-type-filter.no-matches { border: 1px solid ${styles.listFilterWidgetNoMatchesOutline}; }`);
}
if (styles.listMatchesShadow) {
content.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`);
}
if (styles.tableColumnsBorder) {
content.push(`
.monaco-table:hover > .monaco-split-view2,
@ -934,8 +920,8 @@ export class DefaultStyleController implements IStyleController {
}
export interface IListOptionsUpdate extends IListViewOptionsUpdate {
readonly enableKeyboardNavigation?: boolean;
readonly automaticKeyboardNavigation?: boolean;
readonly typeNavigationEnabled?: boolean;
readonly typeNavigationMode?: TypeNavigationMode;
readonly multipleSelectionSupport?: boolean;
}
@ -964,7 +950,7 @@ export interface IListOptions<T> extends IListOptionsUpdate {
readonly alwaysConsumeMouseWheel?: boolean;
}
export interface IListStyles {
export interface IListStyles extends IFindInputStyles {
listBackground?: Color;
listFocusBackground?: Color;
listFocusForeground?: Color;
@ -989,7 +975,7 @@ export interface IListStyles {
listFilterWidgetBackground?: Color;
listFilterWidgetOutline?: Color;
listFilterWidgetNoMatchesOutline?: Color;
listMatchesShadow?: Color;
listFilterWidgetShadow?: Color;
treeIndentGuidesStroke?: Color;
tableColumnsBorder?: Color;
tableOddRowsBackgroundColor?: Color;
@ -1247,7 +1233,7 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
protected view: ListView<T>;
private spliceable: ISpliceable<T>;
private styleController: IStyleController;
private typeLabelController?: TypeLabelController<T>;
private typeNavigationController?: TypeNavigationController<T>;
private accessibilityProvider?: IListAccessibilityProvider<T>;
private keyboardController: KeyboardController<T> | undefined;
private mouseController: MouseController<T>;
@ -1387,8 +1373,8 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
if (_options.keyboardNavigationLabelProvider) {
const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate;
this.typeLabelController = new TypeLabelController(this, this.view, _options.keyboardNavigationLabelProvider, delegate);
this.disposables.add(this.typeLabelController);
this.typeNavigationController = new TypeNavigationController(this, this.view, _options.keyboardNavigationLabelProvider, delegate);
this.disposables.add(this.typeNavigationController);
}
this.mouseController = this.createMouseController(_options);
@ -1413,7 +1399,7 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
updateOptions(optionsUpdate: IListOptionsUpdate = {}): void {
this._options = { ...this._options, ...optionsUpdate };
this.typeLabelController?.updateOptions(this._options);
this.typeNavigationController?.updateOptions(this._options);
if (this._options.multipleSelectionController !== undefined) {
if (this._options.multipleSelectionSupport) {
@ -1529,9 +1515,9 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
this.view.layout(height, width);
}
toggleKeyboardNavigation(): void {
if (this.typeLabelController) {
this.typeLabelController.toggle();
triggerTypeNavigation(): void {
if (this.typeNavigationController) {
this.typeNavigationController.trigger();
}
}

View file

@ -33,8 +33,7 @@ export const MENU_ESCAPED_MNEMONIC_REGEX = /(&amp;)?(&amp;)([^\s&])/g;
export enum Direction {
Right,
Left,
Down
Left
}
export interface IMenuOptions {

View file

@ -13,6 +13,10 @@
overflow: hidden;
}
.menubar.overflow-menu-only {
width: 38px;
}
.fullscreen .menubar:not(.compact) {
margin: 0px;
padding: 4px 5px;
@ -93,6 +97,7 @@
justify-content: center;
}
.menubar:not(.compact) .menubar-menu-button:first-child .toolbar-toggle-more::before,
.menubar.compact .toolbar-toggle-more::before {
content: "\eb94" !important;
}

View file

@ -334,8 +334,6 @@ export class MenuBar extends Disposable {
triggerKeys.push(KeyCode.RightArrow);
} else if (this.options.compactMode === Direction.Left) {
triggerKeys.push(KeyCode.LeftArrow);
} else if (this.options.compactMode === Direction.Down) {
triggerKeys.push(KeyCode.DownArrow);
}
}
@ -475,6 +473,11 @@ export class MenuBar extends Disposable {
return;
}
const overflowMenuOnlyClass = 'overflow-menu-only';
// Remove overflow only restriction to allow the most space
this.container.classList.toggle(overflowMenuOnlyClass, false);
const sizeAvailable = this.container.offsetWidth;
let currentSize = 0;
let full = this.isCompact;
@ -501,6 +504,18 @@ export class MenuBar extends Disposable {
}
}
// If below minimium menu threshold, show the overflow menu only as hamburger menu
if (this.numMenusShown - 1 <= showableMenus.length / 2) {
for (const menuBarMenu of showableMenus) {
menuBarMenu.buttonElement.style.visibility = 'hidden';
}
full = true;
this.numMenusShown = 0;
currentSize = 0;
}
// Overflow
if (this.isCompact) {
this.overflowMenu.actions = [];
@ -540,6 +555,9 @@ export class MenuBar extends Disposable {
this.container.appendChild(this.overflowMenu.buttonElement);
this.overflowMenu.buttonElement.style.visibility = 'hidden';
}
// If we are only showing the overflow, add this class to avoid taking up space
this.container.classList.toggle(overflowMenuOnlyClass, this.numMenusShown === 0);
}
private updateLabels(titleElement: HTMLElement, buttonElement: HTMLElement, label: string): void {

View file

@ -265,8 +265,8 @@ export class Table<TRow> implements ISpliceable<TRow>, IThemable, IDisposable {
this.list.layout(listHeight, width);
}
toggleKeyboardNavigation(): void {
this.list.toggleKeyboardNavigation();
triggerTypeNavigation(): void {
this.list.triggerTypeNavigation();
}
style(styles: ITableStyles): void {

View file

@ -148,6 +148,7 @@ export class Toggle extends Widget {
this.checked = !this._checked;
this._onChange.fire(true);
keyboardEvent.preventDefault();
keyboardEvent.stopPropagation();
return;
}

View file

@ -3,27 +3,34 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DragAndDropData, IDragAndDropData, StaticDND } from 'vs/base/browser/dnd';
import { $, addDisposableListener, append, clearNode, createStyleSheet, getDomNodePagePosition, hasParentWithClass } from 'vs/base/browser/dom';
import { IDragAndDropData } from 'vs/base/browser/dnd';
import { $, append, clearNode, createStyleSheet, h, hasParentWithClass } from 'vs/base/browser/dom';
import { DomEmitter } from 'vs/base/browser/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IIdentityProvider, IKeyboardNavigationDelegate, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput';
import { IMessage, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { DefaultKeyboardNavigationDelegate, IListOptions, IListStyles, isButton, isInputElement, isMonacoEditor, List, MouseController } from 'vs/base/browser/ui/list/listWidget';
import { IListOptions, IListStyles, isButton, isInputElement, isMonacoEditor, List, MouseController, TypeNavigationMode } from 'vs/base/browser/ui/list/listWidget';
import { Toggle } from 'vs/base/browser/ui/toggle/toggle';
import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTreeModel';
import { ICollapseStateChangeEvent, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeEvent, ITreeFilter, ITreeModel, ITreeModelSpliceEvent, ITreeMouseEvent, ITreeNavigator, ITreeNode, ITreeRenderer, TreeDragOverBubble, TreeError, TreeFilterResult, TreeMouseEventTarget, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
import { Action } from 'vs/base/common/actions';
import { distinct, equals, firstOrDefault, range } from 'vs/base/common/arrays';
import { disposableTimeout } from 'vs/base/common/async';
import { disposableTimeout, timeout } from 'vs/base/common/async';
import { Codicon } from 'vs/base/common/codicons';
import { SetMap } from 'vs/base/common/collections';
import { Color } from 'vs/base/common/color';
import { Emitter, Event, EventBufferer, Relay } from 'vs/base/common/event';
import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { clamp } from 'vs/base/common/numbers';
import { isMacintosh } from 'vs/base/common/platform';
import { ScrollEvent } from 'vs/base/common/scrollable';
import { ISpliceable } from 'vs/base/common/sequence';
import { isNumber } from 'vs/base/common/types';
import 'vs/css!./media/tree';
import { localize } from 'vs/nls';
@ -194,8 +201,7 @@ function asListOptions<T, TFilterData, TRef>(modelProvider: () => ITreeModel<T,
getKeyboardNavigationLabel(node) {
return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(node.element);
}
},
enableKeyboardNavigation: options.simpleKeyboardNavigation
}
};
}
@ -563,7 +569,7 @@ class TreeRenderer<T, TFilterData, TRef, TTemplateData> implements IListRenderer
export type LabelFuzzyScore = { label: string; score: FuzzyScore };
class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDisposable {
class FindFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDisposable {
private _totalCount = 0;
get totalCount(): number { return this._totalCount; }
private _matchCount = 0;
@ -590,10 +596,6 @@ class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDi
if (this._filter) {
const result = this._filter.filter(element, parentVisibility);
if (this.tree.options.simpleKeyboardNavigation) {
return result;
}
let visibility: TreeVisibility;
if (typeof result === 'boolean') {
@ -611,7 +613,7 @@ class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDi
this._totalCount++;
if (this.tree.options.simpleKeyboardNavigation || !this._pattern) {
if (!this._pattern) {
this._matchCount++;
return { data: FuzzyScore.Default, visibility: true };
}
@ -634,7 +636,7 @@ class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDi
}
}
if (this.tree.options.filterOnType) {
if (this.tree.findMode === TreeFindMode.Filter) {
return TreeVisibility.Recurse;
} else {
return { data: FuzzyScore.Default, visibility: true };
@ -651,170 +653,259 @@ class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDi
}
}
class TypeFilterController<T, TFilterData> implements IDisposable {
export interface ICaseSensitiveToggleOpts {
readonly isChecked: boolean;
readonly inputActiveOptionBorder?: Color;
readonly inputActiveOptionForeground?: Color;
readonly inputActiveOptionBackground?: Color;
}
private _enabled = false;
get enabled(): boolean { return this._enabled; }
export class ModeToggle extends Toggle {
constructor(opts?: ICaseSensitiveToggleOpts) {
super({
icon: Codicon.filter,
title: localize('filter', "Filter"),
isChecked: opts?.isChecked ?? false,
inputActiveOptionBorder: opts?.inputActiveOptionBorder,
inputActiveOptionForeground: opts?.inputActiveOptionForeground,
inputActiveOptionBackground: opts?.inputActiveOptionBackground
});
}
}
export interface IFindWidgetStyles extends IFindInputStyles, IListStyles { }
export interface IFindWidgetOpts extends IFindWidgetStyles { }
export enum TreeFindMode {
Highlight,
Filter
}
class FindWidget<T, TFilterData> extends Disposable {
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) {
this.modeToggle.checked = mode === TreeFindMode.Filter;
this.findInput.inputBox.setPlaceHolder(mode === TreeFindMode.Filter ? localize('type to filter', "Type to filter") : localize('type to search', "Type to search"));
}
private readonly modeToggle: ModeToggle;
private readonly findInput: FindInput;
private readonly actionbar: ActionBar;
private width = 0;
private right = 4;
readonly _onDidDisable = new Emitter<void>();
readonly onDidDisable = this._onDidDisable.event;
readonly onDidChangeValue: Event<string>;
readonly onDidChangeMode: Event<TreeFindMode>;
constructor(
container: HTMLElement,
private tree: AbstractTree<T, TFilterData, any>,
contextViewProvider: IContextViewProvider,
mode: TreeFindMode,
options?: IFindWidgetOpts
) {
super();
container.appendChild(this.elements.root);
this._register(toDisposable(() => container.removeChild(this.elements.root)));
this.modeToggle = this._register(new ModeToggle({ ...options, isChecked: mode === TreeFindMode.Filter }));
this.onDidChangeMode = Event.map(this.modeToggle.onChange, () => this.modeToggle.checked ? TreeFindMode.Filter : TreeFindMode.Highlight, this._store);
this.findInput = this._register(new FindInput(this.elements.findInput, contextViewProvider, false, {
label: localize('type to search', "Type to search"),
additionalToggles: [this.modeToggle]
}));
this.actionbar = this._register(new ActionBar(this.elements.actionbar));
this.mode = mode;
const emitter = this._register(new DomEmitter(this.findInput.inputBox.inputElement, 'keydown'));
const onKeyDown = this._register(Event.chain(emitter.event))
.map(e => new StandardKeyboardEvent(e))
.event;
this._register(onKeyDown((e): any => {
switch (e.keyCode) {
case KeyCode.DownArrow:
e.preventDefault();
e.stopPropagation();
this.tree.domFocus();
return;
}
}));
const closeAction = this._register(new Action('close', localize('close', "Close"), 'codicon codicon-close', true, () => this.dispose()));
this.actionbar.push(closeAction, { icon: true, label: false });
const onGrabMouseDown = this._register(new DomEmitter(this.elements.grab, 'mousedown'));
this._register(onGrabMouseDown.event(e => {
const disposables = new DisposableStore();
const onWindowMouseMove = disposables.add(new DomEmitter(window, 'mousemove'));
const onWindowMouseUp = disposables.add(new DomEmitter(window, 'mouseup'));
const startRight = this.right;
const startX = e.pageX;
this.elements.grab.classList.add('grabbing');
const update = (e: MouseEvent) => {
const deltaX = e.pageX - startX;
this.right = startRight - deltaX;
this.layout();
};
disposables.add(onWindowMouseMove.event(update));
disposables.add(onWindowMouseUp.event(e => {
update(e);
this.elements.grab.classList.remove('grabbing');
disposables.dispose();
}));
}));
this.onDidChangeValue = this.findInput.onDidChange;
this.style(options ?? {});
}
style(styles: IFindWidgetStyles): void {
this.findInput.style(styles);
if (styles.listFilterWidgetBackground) {
this.elements.root.style.backgroundColor = styles.listFilterWidgetBackground.toString();
}
if (styles.listFilterWidgetShadow) {
this.elements.root.style.boxShadow = `0 0 8px 2px ${styles.listFilterWidgetShadow}`;
}
}
focus() {
this.findInput.focus();
}
select() {
this.findInput.select();
}
layout(width: number = this.width): void {
this.width = width;
this.right = Math.min(Math.max(20, this.right), Math.max(20, width - 170));
this.elements.root.style.right = `${this.right}px`;
}
showMessage(message: IMessage): void {
this.findInput.showMessage(message);
}
clearMessage(): void {
this.findInput.clearMessage();
}
override async dispose(): Promise<void> {
this._onDidDisable.fire();
this.elements.root.classList.add('disabled');
await timeout(300);
super.dispose();
}
}
class FindController<T, TFilterData> implements IDisposable {
private _pattern = '';
get pattern(): string { return this._pattern; }
private _filterOnType: boolean;
get filterOnType(): boolean { return this._filterOnType; }
private _mode: TreeFindMode;
get mode(): TreeFindMode { return this._mode; }
set mode(mode: TreeFindMode) {
if (mode === this._mode) {
return;
}
private _empty: boolean = false;
get empty(): boolean { return this._empty; }
this._mode = mode;
private readonly _onDidChangeEmptyState = new Emitter<boolean>();
readonly onDidChangeEmptyState: Event<boolean> = Event.latch(this._onDidChangeEmptyState.event);
if (this.widget) {
this.widget.mode = this._mode;
}
private positionClassName = 'ne';
private domNode: HTMLElement;
private messageDomNode: HTMLElement;
private labelDomNode: HTMLElement;
private filterOnTypeDomNode: HTMLInputElement;
private clearDomNode: HTMLElement;
private keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
this.tree.refilter();
this.render();
this._onDidChangeMode.fire(mode);
}
private automaticKeyboardNavigation = true;
private triggered = false;
private widget: FindWidget<T, TFilterData> | undefined;
private styles: IFindWidgetStyles | undefined;
private width = 0;
private readonly _onDidChangeMode = new Emitter<TreeFindMode>();
readonly onDidChangeMode = this._onDidChangeMode.event;
private readonly _onDidChangePattern = new Emitter<string>();
readonly onDidChangePattern = this._onDidChangePattern.event;
private readonly enabledDisposables = new DisposableStore();
private readonly _onDidChangeOpenState = new Emitter<boolean>();
readonly onDidChangeOpenState = this._onDidChangeOpenState.event;
private enabledDisposables = new DisposableStore();
private readonly disposables = new DisposableStore();
constructor(
private tree: AbstractTree<T, TFilterData, any>,
model: ITreeModel<T, TFilterData, any>,
private view: List<ITreeNode<T, TFilterData>>,
private filter: TypeFilter<T>,
private keyboardNavigationDelegate: IKeyboardNavigationDelegate
private filter: FindFilter<T>,
private readonly contextViewProvider: IContextViewProvider
) {
this.domNode = $(`.monaco-list-type-filter.${this.positionClassName}`);
this.domNode.draggable = true;
this.disposables.add(addDisposableListener(this.domNode, 'dragstart', () => this.onDragStart()));
this.messageDomNode = append(view.getHTMLElement(), $(`.monaco-list-type-filter-message`));
this.labelDomNode = append(this.domNode, $('span.label'));
const controls = append(this.domNode, $('.controls'));
this._filterOnType = !!tree.options.filterOnType;
this.filterOnTypeDomNode = append(controls, $<HTMLInputElement>('input.filter'));
this.filterOnTypeDomNode.type = 'checkbox';
this.filterOnTypeDomNode.checked = this._filterOnType;
this.filterOnTypeDomNode.tabIndex = -1;
this.updateFilterOnTypeTitleAndIcon();
this.disposables.add(addDisposableListener(this.filterOnTypeDomNode, 'input', () => this.onDidChangeFilterOnType()));
this.clearDomNode = append(controls, $<HTMLInputElement>('button.clear' + Codicon.treeFilterClear.cssSelector));
this.clearDomNode.tabIndex = -1;
this.clearDomNode.title = localize('clear', "Clear");
this.keyboardNavigationEventFilter = tree.options.keyboardNavigationEventFilter;
this._mode = tree.options.defaultFindMode ?? TreeFindMode.Highlight;
model.onDidSplice(this.onDidSpliceModel, this, this.disposables);
this.updateOptions(tree.options);
}
updateOptions(options: IAbstractTreeOptions<T, TFilterData>): void {
if (options.simpleKeyboardNavigation) {
this.disable();
} else {
this.enable();
}
if (typeof options.filterOnType !== 'undefined') {
this._filterOnType = !!options.filterOnType;
this.filterOnTypeDomNode.checked = this._filterOnType;
this.updateFilterOnTypeTitleAndIcon();
}
if (typeof options.automaticKeyboardNavigation !== 'undefined') {
this.automaticKeyboardNavigation = options.automaticKeyboardNavigation;
}
this.tree.refilter();
this.render();
if (!this.automaticKeyboardNavigation) {
this.onEventOrInput('');
}
}
toggle(): void {
this.triggered = !this.triggered;
if (!this.triggered) {
this.onEventOrInput('');
}
}
private enable(): void {
if (this._enabled) {
open(): void {
if (this.widget) {
this.widget.focus();
this.widget.select();
return;
}
const onRawKeyDown = this.enabledDisposables.add(new DomEmitter(this.view.getHTMLElement(), 'keydown'));
const onKeyDown = Event.chain(onRawKeyDown.event)
.filter(e => !isInputElement(e.target as HTMLElement) || e.target === this.filterOnTypeDomNode)
.filter(e => e.key !== 'Dead' && !/^Media/.test(e.key))
.map(e => new StandardKeyboardEvent(e))
.filter(this.keyboardNavigationEventFilter || (() => true))
.filter(() => this.automaticKeyboardNavigation || this.triggered)
.filter(e => (this.keyboardNavigationDelegate.mightProducePrintableCharacter(e) && !(e.keyCode === KeyCode.DownArrow || e.keyCode === KeyCode.UpArrow || e.keyCode === KeyCode.LeftArrow || e.keyCode === KeyCode.RightArrow)) || ((this.pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? (e.altKey && !e.metaKey) : e.ctrlKey) && !e.shiftKey)))
.forEach(e => { e.stopPropagation(); e.preventDefault(); })
.event;
this.widget = new FindWidget(this.view.getHTMLElement(), this.tree, this.contextViewProvider, this._mode, this.styles);
this.enabledDisposables.add(this.widget);
const onClearClick = this.enabledDisposables.add(new DomEmitter(this.clearDomNode, 'click'));
this.widget.onDidChangeValue(this.onDidChangeValue, this, this.enabledDisposables);
this.widget.onDidChangeMode(mode => this.mode = mode, undefined, this.enabledDisposables);
this.widget.onDidDisable(this.close, this, this.enabledDisposables);
Event.chain(Event.any<MouseEvent | StandardKeyboardEvent>(onKeyDown, onClearClick.event))
.event(this.onEventOrInput, this, this.enabledDisposables);
this.widget.layout(this.width);
this.widget.focus();
this.filter.pattern = '';
this.tree.refilter();
this.render();
this._enabled = true;
this.triggered = false;
this._onDidChangeOpenState.fire(true);
}
private disable(): void {
if (!this._enabled) {
close(): void {
if (!this.widget) {
return;
}
this.domNode.remove();
this.enabledDisposables.clear();
this.tree.refilter();
this.render();
this._enabled = false;
this.triggered = false;
this.widget = undefined;
this.enabledDisposables.dispose();
this.enabledDisposables = new DisposableStore();
this.onDidChangeValue('');
this.tree.domFocus();
this._onDidChangeOpenState.fire(false);
}
private onEventOrInput(e: MouseEvent | StandardKeyboardEvent | string): void {
if (typeof e === 'string') {
this.onInput(e);
} else if (e instanceof MouseEvent || e.keyCode === KeyCode.Escape || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey))) {
this.onInput('');
} else if (e.keyCode === KeyCode.Backspace) {
this.onInput(this.pattern.length === 0 ? '' : this.pattern.substr(0, this.pattern.length - 1));
} else {
this.onInput(this.pattern + e.browserEvent.key);
}
}
private onInput(pattern: string): void {
const container = this.view.getHTMLElement();
if (pattern && !this.domNode.parentElement) {
container.append(this.domNode);
} else if (!pattern && this.domNode.parentElement) {
this.domNode.remove();
this.tree.domFocus();
}
private onDidChangeValue(pattern: string): void {
this._pattern = pattern;
this._onDidChangePattern.fire(pattern);
@ -836,75 +927,10 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
}
this.render();
if (!pattern) {
this.triggered = false;
}
}
private onDragStart(): void {
const container = this.view.getHTMLElement();
const { left } = getDomNodePagePosition(container);
const containerWidth = container.clientWidth;
const midContainerWidth = containerWidth / 2;
const width = this.domNode.clientWidth;
const disposables = new DisposableStore();
let positionClassName = this.positionClassName;
const updatePosition = () => {
switch (positionClassName) {
case 'nw':
this.domNode.style.top = `4px`;
this.domNode.style.left = `4px`;
break;
case 'ne':
this.domNode.style.top = `4px`;
this.domNode.style.left = `${containerWidth - width - 6}px`;
break;
}
};
const onDragOver = (event: DragEvent) => {
event.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
const x = event.clientX - left;
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'none';
}
if (x < midContainerWidth) {
positionClassName = 'nw';
} else {
positionClassName = 'ne';
}
updatePosition();
};
const onDragEnd = () => {
this.positionClassName = positionClassName;
this.domNode.className = `monaco-list-type-filter ${this.positionClassName}`;
this.domNode.style.top = '';
this.domNode.style.left = '';
dispose(disposables);
};
updatePosition();
this.domNode.classList.remove(positionClassName);
this.domNode.classList.add('dragging');
disposables.add(toDisposable(() => this.domNode.classList.remove('dragging')));
disposables.add(addDisposableListener(document, 'dragover', e => onDragOver(e)));
disposables.add(addDisposableListener(this.domNode, 'dragend', () => onDragEnd()));
StaticDND.CurrentDragAndDropData = new DragAndDropData('vscode-ui');
disposables.add(toDisposable(() => StaticDND.CurrentDragAndDropData = undefined));
}
private onDidSpliceModel(): void {
if (!this._enabled || this.pattern.length === 0) {
if (!this.widget || this.pattern.length === 0) {
return;
}
@ -912,46 +938,18 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
this.render();
}
private onDidChangeFilterOnType(): void {
this.tree.updateOptions({ filterOnType: this.filterOnTypeDomNode.checked });
this.tree.refilter();
this.tree.domFocus();
this.render();
this.updateFilterOnTypeTitleAndIcon();
}
private updateFilterOnTypeTitleAndIcon(): void {
if (this.filterOnType) {
this.filterOnTypeDomNode.classList.remove(...Codicon.treeFilterOnTypeOff.classNamesArray);
this.filterOnTypeDomNode.classList.add(...Codicon.treeFilterOnTypeOn.classNamesArray);
this.filterOnTypeDomNode.title = localize('disable filter on type', "Disable Filter on Type");
} else {
this.filterOnTypeDomNode.classList.remove(...Codicon.treeFilterOnTypeOn.classNamesArray);
this.filterOnTypeDomNode.classList.add(...Codicon.treeFilterOnTypeOff.classNamesArray);
this.filterOnTypeDomNode.title = localize('enable filter on type', "Enable Filter on Type");
}
}
private render(): void {
const noMatches = this.filter.totalCount > 0 && this.filter.matchCount === 0;
if (this.pattern && this.tree.options.filterOnType && noMatches) {
this.messageDomNode.textContent = localize('empty', "No elements found");
this._empty = true;
if (this.pattern && noMatches) {
this.widget?.showMessage({ type: MessageType.WARNING, content: localize('not found', "No elements found.") });
} else {
this.messageDomNode.innerText = '';
this._empty = false;
this.widget?.clearMessage();
}
this.domNode.classList.toggle('no-matches', noMatches);
this.domNode.title = localize('found', "Matched {0} out of {1} elements", this.filter.matchCount, this.filter.totalCount);
this.labelDomNode.textContent = this.pattern.length > 16 ? '…' + this.pattern.substr(this.pattern.length - 16) : this.pattern;
this._onDidChangeEmptyState.fire(this._empty);
}
shouldAllowFocus(node: ITreeNode<T, TFilterData>): boolean {
if (!this.enabled || !this.pattern || this.filterOnType) {
if (!this.widget || !this.pattern || this._mode === TreeFindMode.Filter) {
return true;
}
@ -962,16 +960,20 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore);
}
dispose() {
if (this._enabled) {
this.domNode.remove();
this.enabledDisposables.dispose();
this._enabled = false;
this.triggered = false;
}
style(styles: IFindWidgetStyles): void {
this.styles = styles;
this.widget?.style(styles);
}
layout(width: number): void {
this.width = width;
this.widget?.layout(width);
}
dispose() {
this._onDidChangePattern.dispose();
dispose(this.disposables);
this.enabledDisposables.dispose();
this.disposables.dispose();
}
}
@ -982,6 +984,8 @@ function asTreeMouseEvent<T>(event: IListMouseEvent<ITreeNode<T, any>>): ITreeMo
target = TreeMouseEventTarget.Twistie;
} else if (hasParentWithClass(event.browserEvent.target as HTMLElement, 'monaco-tl-contents', 'monaco-tl-row')) {
target = TreeMouseEventTarget.Element;
} else if (hasParentWithClass(event.browserEvent.target as HTMLElement, 'monaco-tree-type-filter', 'monaco-list')) {
target = TreeMouseEventTarget.Filter;
}
return {
@ -1005,9 +1009,9 @@ export interface IKeyboardNavigationEventFilter {
export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions {
readonly multipleSelectionSupport?: boolean;
readonly automaticKeyboardNavigation?: boolean;
readonly simpleKeyboardNavigation?: boolean;
readonly filterOnType?: boolean;
readonly typeNavigationEnabled?: boolean;
readonly typeNavigationMode?: TypeNavigationMode;
readonly defaultFindMode?: TreeFindMode;
readonly smoothScrolling?: boolean;
readonly horizontalScrolling?: boolean;
readonly mouseWheelScrollSensitivity?: number;
@ -1017,6 +1021,7 @@ export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions {
}
export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTreeOptionsUpdate, IListOptions<T> {
readonly contextViewProvider?: IContextViewProvider;
readonly collapseByDefault?: boolean; // defaults to false
readonly filter?: ITreeFilter<T, TFilterData>;
readonly dnd?: ITreeDragAndDrop<T>;
@ -1318,7 +1323,8 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
private selection: Trait<T>;
private anchor: Trait<T>;
private eventBufferer = new EventBufferer();
private typeFilterController?: TypeFilterController<T, TFilterData>;
private findController?: FindController<T, TFilterData>;
readonly onDidChangeFindOpenState: Event<boolean> = Event.None;
private focusNavigationFilter: ((node: ITreeNode<T, TFilterData>) => boolean) | undefined;
private styleElement: HTMLStyleElement;
protected readonly disposables = new DisposableStore();
@ -1329,7 +1335,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
get onDidChangeSelection(): Event<ITreeEvent<T>> { return this.eventBufferer.wrapEvent(this.selection.onDidChange); }
get onMouseClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onMouseClick, asTreeMouseEvent); }
get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onMouseDblClick, asTreeMouseEvent); }
get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return Event.filter(Event.map(this.view.onMouseDblClick, asTreeMouseEvent), e => e.target !== TreeMouseEventTarget.Filter); }
get onContextMenu(): Event<ITreeContextMenuEvent<T>> { return Event.map(this.view.onContextMenu, asTreeContextMenuEvent); }
get onTap(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onTap, asTreeMouseEvent); }
get onPointer(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onPointer, asTreeMouseEvent); }
@ -1348,8 +1354,11 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
private readonly _onWillRefilter = new Emitter<void>();
readonly onWillRefilter: Event<void> = this._onWillRefilter.event;
get filterOnType(): boolean { return !!this._options.filterOnType; }
get onDidChangeTypeFilterPattern(): Event<string> { return this.typeFilterController ? this.typeFilterController.onDidChangePattern : Event.None; }
get findMode(): TreeFindMode { return this.findController?.mode ?? TreeFindMode.Highlight; }
set findMode(findMode: TreeFindMode) { if (this.findController) { this.findController.mode = findMode; } }
readonly onDidChangeFindMode: Event<TreeFindMode>;
get onDidChangeFindPattern(): Event<string> { return this.findController ? this.findController.onDidChangePattern : Event.None; }
get expandOnDoubleClick(): boolean { return typeof this._options.expandOnDoubleClick === 'undefined' ? true : this._options.expandOnDoubleClick; }
get expandOnlyOnTwistieClick(): boolean | ((e: T) => boolean) { return typeof this._options.expandOnlyOnTwistieClick === 'undefined' ? true : this._options.expandOnlyOnTwistieClick; }
@ -1376,10 +1385,10 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
this.disposables.add(r);
}
let filter: TypeFilter<T> | undefined;
let filter: FindFilter<T> | undefined;
if (_options.keyboardNavigationLabelProvider) {
filter = new TypeFilter(this, _options.keyboardNavigationLabelProvider, _options.filter as any as ITreeFilter<T, FuzzyScore>);
filter = new FindFilter(this, _options.keyboardNavigationLabelProvider, _options.filter as any as ITreeFilter<T, FuzzyScore>);
_options = { ..._options, filter: filter as ITreeFilter<T, TFilterData> }; // TODO need typescript help here
this.disposables.add(filter);
}
@ -1432,11 +1441,14 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables);
}
if (_options.keyboardNavigationLabelProvider) {
const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate;
this.typeFilterController = new TypeFilterController(this, this.model, this.view, filter!, delegate);
this.focusNavigationFilter = node => this.typeFilterController!.shouldAllowFocus(node);
this.disposables.add(this.typeFilterController!);
if (_options.keyboardNavigationLabelProvider && _options.contextViewProvider) {
this.findController = new FindController(this, this.model, this.view, filter!, _options.contextViewProvider);
this.focusNavigationFilter = node => this.findController!.shouldAllowFocus(node);
this.onDidChangeFindOpenState = this.findController.onDidChangeOpenState;
this.disposables.add(this.findController!);
this.onDidChangeFindMode = this.findController.onDidChangeMode;
} else {
this.onDidChangeFindMode = Event.None;
}
this.styleElement = createStyleSheet(this.view.getHTMLElement());
@ -1450,13 +1462,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
renderer.updateOptions(optionsUpdate);
}
this.view.updateOptions({
...this._options,
enableKeyboardNavigation: this._options.simpleKeyboardNavigation,
});
this.typeFilterController?.updateOptions(this._options);
this.view.updateOptions(this._options);
this._onDidUpdateOptions.fire(this._options);
this.getHTMLElement().classList.toggle('always', this._options.renderIndentGuides === RenderIndentGuides.Always);
@ -1483,21 +1489,11 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
}
get contentHeight(): number {
if (this.typeFilterController && this.typeFilterController.filterOnType && this.typeFilterController.empty) {
return 100;
}
return this.view.contentHeight;
}
get onDidChangeContentHeight(): Event<number> {
let result = this.view.onDidChangeContentHeight;
if (this.typeFilterController) {
result = Event.any(result, Event.map(this.typeFilterController.onDidChangeEmptyState, () => this.contentHeight));
}
return result;
return this.view.onDidChangeContentHeight;
}
get scrollTop(): number {
@ -1559,6 +1555,10 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
layout(height?: number, width?: number): void {
this.view.layout(height, width);
if (isNumber(width)) {
this.findController?.layout(width);
}
}
style(styles: IListStyles): void {
@ -1571,6 +1571,8 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
}
this.styleElement.textContent = content.join('\n');
this.findController?.style(styles);
this.view.style(styles);
}
@ -1624,12 +1626,16 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
return this.model.isCollapsed(location);
}
toggleKeyboardNavigation(): void {
this.view.toggleKeyboardNavigation();
triggerTypeNavigation(): void {
this.view.triggerTypeNavigation();
}
if (this.typeFilterController) {
this.typeFilterController.toggle();
}
openFind(): void {
this.findController?.open();
}
closeFind(): void {
this.findController?.close();
}
refilter(): void {

View file

@ -7,7 +7,7 @@ import { IDragAndDropData } from 'vs/base/browser/dnd';
import { IIdentityProvider, IListDragAndDrop, IListDragOverReaction, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
import { ComposedTreeDelegate, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree';
import { ComposedTreeDelegate, TreeFindMode as TreeFindMode, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree';
import { ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTreeModel';
import { CompressibleObjectTree, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions, ICompressibleTreeRenderer, IObjectTreeOptions, IObjectTreeSetChildrenOptions, ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
@ -341,7 +341,12 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
get onDidUpdateOptions(): Event<IAsyncDataTreeOptionsUpdate> { return this.tree.onDidUpdateOptions; }
get filterOnType(): boolean { return this.tree.filterOnType; }
get onDidChangeFindOpenState(): Event<boolean> { return this.tree.onDidChangeFindOpenState; }
get findMode(): TreeFindMode { return this.tree.findMode; }
set findMode(mode: TreeFindMode) { this.tree.findMode = mode; }
readonly onDidChangeFindMode: Event<TreeFindMode>;
get expandOnlyOnTwistieClick(): boolean | ((e: T) => boolean) {
if (typeof this.tree.expandOnlyOnTwistieClick === 'boolean') {
return this.tree.expandOnlyOnTwistieClick;
@ -367,6 +372,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
this.collapseByDefault = options.collapseByDefault;
this.tree = this.createTree(user, container, delegate, renderers, options);
this.onDidChangeFindMode = this.tree.onDidChangeFindMode;
this.root = createAsyncDataTreeNode({
element: undefined!,
@ -616,8 +622,16 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
return this.tree.isCollapsed(this.getDataNode(element));
}
toggleKeyboardNavigation(): void {
this.tree.toggleKeyboardNavigation();
triggerTypeNavigation(): void {
this.tree.triggerTypeNavigation();
}
openFind(): void {
this.tree.openFind();
}
closeFind(): void {
this.tree.closeFind();
}
refilter(): void {

View file

@ -67,3 +67,45 @@
/* Use steps to throttle FPS to reduce CPU usage */
animation: codicon-spin 1.25s steps(30) infinite;
}
.monaco-tree-type-filter {
position: absolute;
top: 0;
display: flex;
padding: 3px;
transition: top 0.3s;
width: 160px;
z-index: 100;
}
.monaco-tree-type-filter.disabled {
top: -40px;
}
.monaco-tree-type-filter-grab {
display: flex !important;
align-items: center;
justify-content: center;
cursor: grab;
}
.monaco-tree-type-filter-grab.grabbing {
cursor: grabbing;
}
.monaco-tree-type-filter-input {
flex: 1;
}
.monaco-tree-type-filter-input .monaco-inputbox > .ibwrapper > .input,
.monaco-tree-type-filter-input .monaco-inputbox > .ibwrapper > .mirror {
padding: 2px;
}
.monaco-tree-type-filter-actionbar {
margin-left: 4px;
}
.monaco-tree-type-filter-actionbar .monaco-action-bar .action-label {
padding: 2px;
}

View file

@ -141,7 +141,8 @@ export interface ITreeEvent<T> {
export enum TreeMouseEventTarget {
Unknown,
Twistie,
Element
Element,
Filter
}
export interface ITreeMouseEvent<T> {

View file

@ -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 = {

View file

@ -390,6 +390,9 @@ interface IEnsuredWriteFileOptions extends IWriteFileOptions {
}
let canFlush = true;
export function configureFlushOnWrite(enabled: boolean): void {
canFlush = enabled;
}
// Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk
// We do this in cases where we want to make sure the data is really on disk and
@ -421,7 +424,7 @@ function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, o
// In that case we disable flushing and warn to the console
if (syncError) {
console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError);
canFlush = false;
configureFlushOnWrite(false);
}
return fs.close(fd, closeError => callback(closeError));
@ -455,7 +458,7 @@ export function writeFileSync(path: string, data: string | Buffer, options?: IWr
fs.fdatasyncSync(fd); // https://github.com/microsoft/vscode/issues/9589
} catch (syncError) {
console.warn('[node.js fs] fdatasyncSync is now disabled for this session because it failed: ', syncError);
canFlush = false;
configureFlushOnWrite(false);
}
} finally {
fs.closeSync(fd);

View file

@ -52,7 +52,7 @@ export function listProcesses(rootPid: number): Promise<ProcessItem> {
const ISSUE_REPORTER_HINT = /--vscode-window-kind=issue-reporter/;
const PROCESS_EXPLORER_HINT = /--vscode-window-kind=process-explorer/;
const UTILITY_NETWORK_HINT = /--utility-sub-type=network/;
const UTILITY_EXTENSION_HOST_HINT = /--vscode-utility-kind=extension-host/;
const UTILITY_EXTENSION_HOST_HINT = /--utility-sub-type=node.mojom.NodeService/;
const WINDOWS_CRASH_REPORTER = /--crashes-directory/;
const WINDOWS_PTY = /\\pipe\\winpty-control/;
const WINDOWS_CONSOLE_HOST = /conhost\.exe/;

View file

@ -670,56 +670,57 @@ flakySuite('SQLite Storage Library', function () {
});
test('multiple concurrent writes execute in sequence', async () => {
class TestStorage extends Storage {
getStorage(): IStorageDatabase {
return this.database;
return runWithFakedTimers({}, async () => {
class TestStorage extends Storage {
getStorage(): IStorageDatabase {
return this.database;
}
}
}
const storage = new TestStorage(new SQLiteStorageDatabase(join(testdir, 'storage.db')));
const storage = new TestStorage(new SQLiteStorageDatabase(join(testdir, 'storage.db')));
await storage.init();
await storage.init();
storage.set('foo', 'bar');
storage.set('some/foo/path', 'some/bar/path');
storage.set('foo', 'bar');
storage.set('some/foo/path', 'some/bar/path');
await timeout(2);
await timeout(2);
storage.set('foo1', 'bar');
storage.set('some/foo1/path', 'some/bar/path');
storage.set('foo1', 'bar');
storage.set('some/foo1/path', 'some/bar/path');
await timeout(2);
await timeout(2);
storage.set('foo2', 'bar');
storage.set('some/foo2/path', 'some/bar/path');
storage.set('foo2', 'bar');
storage.set('some/foo2/path', 'some/bar/path');
await timeout(2);
await timeout(2);
storage.delete('foo1');
storage.delete('some/foo1/path');
storage.delete('foo1');
storage.delete('some/foo1/path');
await timeout(2);
await timeout(2);
storage.delete('foo4');
storage.delete('some/foo4/path');
storage.delete('foo4');
storage.delete('some/foo4/path');
await timeout(5);
await timeout(5);
storage.set('foo3', 'bar');
await storage.set('some/foo3/path', 'some/bar/path');
storage.set('foo3', 'bar');
await storage.set('some/foo3/path', 'some/bar/path');
const items = await storage.getStorage().getItems();
strictEqual(items.get('foo'), 'bar');
strictEqual(items.get('some/foo/path'), 'some/bar/path');
strictEqual(items.has('foo1'), false);
strictEqual(items.has('some/foo1/path'), false);
strictEqual(items.get('foo2'), 'bar');
strictEqual(items.get('some/foo2/path'), 'some/bar/path');
strictEqual(items.get('foo3'), 'bar');
strictEqual(items.get('some/foo3/path'), 'some/bar/path');
const items = await storage.getStorage().getItems();
strictEqual(items.get('foo'), 'bar');
strictEqual(items.get('some/foo/path'), 'some/bar/path');
strictEqual(items.has('foo1'), false);
strictEqual(items.has('some/foo1/path'), false);
strictEqual(items.get('foo2'), 'bar');
strictEqual(items.get('some/foo2/path'), 'some/bar/path');
strictEqual(items.get('foo3'), 'bar');
strictEqual(items.get('some/foo3/path'), 'some/bar/path');
await storage.close();
await storage.close();
});
});
test('lots of INSERT & DELETE (below inline max)', async () => {

View file

@ -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);
});
});

View file

@ -11,9 +11,11 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { randomPath } from 'vs/base/common/extpath';
import { join, sep } from 'vs/base/common/path';
import { isWindows } from 'vs/base/common/platform';
import { Promises, RimRafMode, rimrafSync, SymlinkSupport, writeFileSync } from 'vs/base/node/pfs';
import { configureFlushOnWrite, Promises, RimRafMode, rimrafSync, SymlinkSupport, writeFileSync } from 'vs/base/node/pfs';
import { flakySuite, getPathFromAmdModule, getRandomTestPath } from 'vs/base/test/node/testUtils';
configureFlushOnWrite(false); // speed up all unit tests by disabling flush on write
flakySuite('PFS', function () {
let testDir: string;
@ -368,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,

View file

@ -53,7 +53,6 @@ import { FollowerLogService, LoggerChannelClient, LogLevelChannelClient } from '
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { RequestService } from 'vs/platform/request/browser/requestService';
import { IRequestService } from 'vs/platform/request/common/request';
import { ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess';
import { IStorageService } from 'vs/platform/storage/common/storage';
@ -106,6 +105,7 @@ import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/pol
import { UserDataProfilesNativeService } from 'vs/platform/userDataProfile/electron-sandbox/userDataProfile';
import { OneDataSystemWebAppender } from 'vs/platform/telemetry/browser/1dsAppender';
import { DefaultExtensionsProfileInitService } from 'vs/platform/extensionManagement/electron-sandbox/defaultExtensionsProfileInit';
import { SharedProcessRequestService } from 'vs/platform/request/electron-browser/sharedProcessRequestService';
class SharedProcessMain extends Disposable {
@ -254,7 +254,7 @@ class SharedProcessMain extends Disposable {
services.set(IUriIdentityService, new UriIdentityService(fileService));
// Request
services.set(IRequestService, new SyncDescriptor(RequestService));
services.set(IRequestService, new SharedProcessRequestService(mainProcessService, configurationService, logService));
// Checksum
services.set(IChecksumService, new SyncDescriptor(ChecksumService));

View file

@ -104,6 +104,8 @@ import { PolicyChannel } from 'vs/platform/policy/common/policyIpc';
import { IUserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile';
import { IDefaultExtensionsProfileInitService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { DefaultExtensionsProfileInitHandler } from 'vs/platform/extensionManagement/electron-main/defaultExtensionsProfileInit';
import { RequestChannel } from 'vs/platform/request/common/requestIpc';
import { IRequestService } from 'vs/platform/request/common/request';
/**
* The main VS Code application. There will only ever be one instance,
@ -728,6 +730,10 @@ export class CodeApplication extends Disposable {
mainProcessElectronServer.registerChannel('userDataProfiles', userDataProfilesService);
sharedProcessClient.then(client => client.registerChannel('userDataProfiles', userDataProfilesService));
// Request
const requestService = new RequestChannel(accessor.get(IRequestService));
sharedProcessClient.then(client => client.registerChannel('request', requestService));
// Update
const updateChannel = new UpdateChannel(accessor.get(IUpdateService));
mainProcessElectronServer.registerChannel('update', updateChannel);
@ -1006,6 +1012,7 @@ export class CodeApplication extends Disposable {
cli: args,
forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
diffMode: args.diff,
mergeMode: args.merge,
noRecentEntry,
waitMarkerFileURI,
gotoLineMode: args.goto,

View file

@ -8,7 +8,7 @@ import { chmodSync, existsSync, readFileSync, statSync, truncateSync, unlinkSync
import { homedir, release, tmpdir } from 'os';
import type { ProfilingSession, Target } from 'v8-inspect-profiler';
import { Event } from 'vs/base/common/event';
import { isAbsolute, resolve } from 'vs/base/common/path';
import { isAbsolute, resolve, join } from 'vs/base/common/path';
import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform';
import { randomPort } from 'vs/base/common/ports';
import { isString } from 'vs/base/common/types';
@ -24,6 +24,8 @@ import product from 'vs/platform/product/common/product';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { randomPath } from 'vs/base/common/extpath';
import { Utils } from 'vs/platform/profiling/common/profiling';
import { dirname } from 'vs/base/common/resources';
import { FileAccess } from 'vs/base/common/network';
function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean {
return !!argv['install-source']
@ -59,6 +61,25 @@ export async function main(argv: string[]): Promise<any> {
console.log(buildVersionMessage(product.version, product.commit));
}
// Shell integration
else if (args['shell-integration']) {
// 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)"`
case 'bash': file = 'shellIntegration-bash.sh'; break;
// Usage: `if ($env:TERM_PROGRAM -eq "vscode") { . "$(code --shell-integration pwsh)" }`
case 'pwsh': file = 'shellIntegration.ps1'; break;
// Usage: `[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --shell-integration zsh)"`
case 'zsh': file = 'shellIntegration-rc.zsh'; break;
default: throw new Error('Error using --shell-integration: Invalid shell type');
}
console.log(join(dirname(FileAccess.asFileUri('', require)).fsPath, 'out', 'vs', 'workbench', 'contrib', 'terminal', 'browser', 'media', file));
}
// Extensions Management
else if (shouldSpawnCliProcess(args)) {
const cli = await new Promise<IMainCli>((resolve, reject) => require(['vs/code/node/cliProcessMain'], resolve, reject));

View file

@ -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;

View file

@ -2966,7 +2966,7 @@ class EditorQuickSuggestions extends BaseEditorOption<EditorOption.quickSuggesti
},
},
default: defaults,
markdownDescription: nls.localize('quickSuggestions', "Controls whether suggestions should automatically show up while typing. This can be controlled for typing in comments, strings, and other code. Quick suggestion can be configured to show as ghost text or with the suggest widget.")
markdownDescription: nls.localize('quickSuggestions', "Controls whether suggestions should automatically show up while typing. This can be controlled for typing in comments, strings, and other code. Quick suggestion can be configured to show as ghost text or with the suggest widget. Also be aware of the '{0}'-setting which controls if suggestions are triggered by special characters.", `#editor.suggestOnTriggerCharacters#`)
});
this.defaultValue = defaults;
}
@ -4557,7 +4557,7 @@ export const enum EditorOption {
export const EditorOptions = {
acceptSuggestionOnCommitCharacter: register(new EditorBooleanOption(
EditorOption.acceptSuggestionOnCommitCharacter, 'acceptSuggestionOnCommitCharacter', true,
{ markdownDescription: nls.localize('acceptSuggestionOnCommitCharacter', "Controls whether suggestions should be accepted on commit characters. For example, in JavaScript, the semi-colon (`;`) can be a commit character that accepts a suggestion and types that character.") }
{ markdownDescription: nls.localize('acceptSuggestionOnCommitCharacter', "Controls whether suggestions should be accepted on commit characters. For example, in JavaScript, the semi-colon (`; `) can be a commit character that accepts a suggestion and types that character.") }
)),
acceptSuggestionOnEnter: register(new EditorStringEnumOption(
EditorOption.acceptSuggestionOnEnter, 'acceptSuggestionOnEnter',

View file

@ -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) {

View file

@ -46,7 +46,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
this._languageFeaturesService.documentOnDropEditProvider.register('*', new DefaultOnDropProvider(workspaceContextService));
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('workbench.experimental.editor.dropIntoEditor.enabled')) {
if (e.affectsConfiguration('workbench.editor.dropIntoEditor.enabled')) {
this.updateEditorOptions(editor);
}
}));
@ -56,7 +56,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
private updateEditorOptions(editor: ICodeEditor) {
editor.updateOptions({
enableDropIntoEditor: this._configurationService.getValue('workbench.experimental.editor.dropIntoEditor.enabled')
enableDropIntoEditor: this._configurationService.getValue('workbench.editor.dropIntoEditor.enabled')
});
}

View file

@ -102,7 +102,9 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget {
}));
const body = $('.body');
const scrollbar = new DomScrollableElement(body, {});
const scrollbar = new DomScrollableElement(body, {
alwaysConsumeMouseWheel: true,
});
this._register(scrollbar);
wrapper.appendChild(scrollbar.getDomNode());

Some files were not shown because too many files have changed in this diff Show more