Merge remote-tracking branch 'origin' into electron-19.x.y

This commit is contained in:
deepak1556 2022-07-25 13:17:53 +09:00
commit d620fd3aa7
330 changed files with 7480 additions and 5405 deletions

16
.github/commands.json vendored
View file

@ -154,7 +154,7 @@
"IllusionMH"
],
"action": "updateLabels",
"addLabel": "~needs more info"
"addLabel": "~info-needed"
},
{
"type": "comment",
@ -165,14 +165,14 @@
"gjsjohnmurray",
"IllusionMH"
],
"addLabel": "needs more info",
"addLabel": "info-needed",
"comment": "Thanks for creating this issue regarding performance! Please follow this guide to help us diagnose performance issues: https://github.com/microsoft/vscode/wiki/Performance-Issues \n\nHappy Coding!"
},
{
"type": "comment",
"name": "jsDebugLogs",
"action": "updateLabels",
"addLabel": "needs more info",
"addLabel": "info-needed",
"comment": "Please collect trace logs using the following instructions:\n\n> If you're able to, add `\"trace\": true` to your `launch.json` and reproduce the issue. The location of the log file on your disk will be written to the Debug Console. Share that with us.\n>\n> ⚠️ This log file will not contain source code, but will contain file paths. You can drop it into https://microsoft.github.io/vscode-pwa-analyzer/index.html to see what it contains. If you'd rather not share the log publicly, you can email it to connor@xbox.com"
},
{
@ -189,17 +189,17 @@
},
{
"type": "label",
"name": "~needs more info",
"name": "~info-needed",
"action": "updateLabels",
"addLabel": "needs more info",
"removeLabel": "~needs more info",
"addLabel": "info-needed",
"removeLabel": "~info-needed",
"comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!"
},
{
"type": "label",
"name": "~needs version info",
"action": "updateLabels",
"addLabel": "needs more info",
"addLabel": "info-needed",
"removeLabel": "~needs version info",
"comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!"
},
@ -416,7 +416,7 @@
"IllusionMH"
],
"action": "comment",
"addLabel": "needs more info",
"addLabel": "info-needed",
"comment": "Thanks for reporting this issue! Unfortunately, it's hard for us to understand what issue you're seeing. Please help us out by providing a screen recording showing exactly what isn't working as expected. While we can work with most standard formats, `.gif` files are preferred as they are displayed inline on GitHub. You may find https://gifcap.dev helpful as a browser-based gif recording tool.\n\nIf the issue depends on keyboard input, you can help us by enabling screencast mode for the recording (`Developer: Toggle Screencast Mode` in the command palette).\n\nHappy coding!"
},
{

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 #

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
16.14

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

@ -0,0 +1,59 @@
steps:
- checkout: self
fetchDepth: 1
retryCountOnTaskFailure: 3
- task: NodeTool@0
inputs:
versionSpec: "16.x"
- script: |
mkdir -p .build
node build/azure-pipelines/common/computeNodeModulesCacheKey.js $VSCODE_ARCH $ENABLE_TERRAPIN > .build/yarnlockhash
displayName: Prepare yarn cache flags
- task: Cache@2
inputs:
key: "genericNodeModules | $(Agent.OS) | .build/yarnlockhash"
path: .build/node_modules_cache
cacheHitVar: NODE_MODULES_RESTORED
displayName: Restore node_modules cache
- script: |
set -e
tar -xzf .build/node_modules_cache/cache.tgz
condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true'))
displayName: Extract node_modules cache
- script: |
set -e
npx https://aka.ms/enablesecurefeed standAlone
timeoutInMinutes: 5
retryCountOnTaskFailure: 3
condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['ENABLE_TERRAPIN'], 'true'))
displayName: Switch to Terrapin packages
- script: |
set -e
for i in {1..3}; do # try 3 times, for Terrapin
yarn --frozen-lockfile --check-files && break
if [ $i -eq 3 ]; then
echo "Yarn failed too many times" >&2
exit 1
fi
echo "Yarn failed $i, trying again..."
done
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
GITHUB_TOKEN: "$(github-distro-mixin-password)"
displayName: Install dependencies
condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'))
- script: |
set -e
node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt
mkdir -p .build/node_modules_cache
tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt
condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'))
displayName: Create node_modules archive

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
@ -22,6 +13,8 @@ variables:
value: true
- name: ENABLE_TERRAPIN
value: false
- name: VSCODE_CIBUILD
value: ${{ in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI') }}
- name: VSCODE_PUBLISH
value: false
- name: VSCODE_QUALITY
@ -30,172 +23,173 @@ variables:
value: false
stages:
- stage: Compile
jobs:
- job: Compile
pool: vscode-1es-vscode-linux-18.04
variables:
VSCODE_ARCH: x64
steps:
- template: product-compile.yml
parameters:
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
- ${{ if eq(variables['VSCODE_CIBUILD'], true) }}:
- stage: MaintainNodeModulesCache
displayName: Maintain node_modules cache
jobs:
- job: MaintainNodeModulesCache
displayName: Maintain node_modules cache
pool: vscode-1es-vscode-linux-20.04
steps:
- template: product-build-pr-cache.yml
- stage: LinuxServerDependencies
dependsOn: []
pool: vscode-1es-vscode-linux-18.04
jobs:
- job: x64
container: centos7-devtoolset8-x64
variables:
VSCODE_ARCH: x64
NPM_ARCH: x64
steps:
- template: linux/product-build-linux-server.yml
parameters:
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
- ${{ if ne(variables['VSCODE_CIBUILD'], true) }}:
- stage: Compile
displayName: Compile & Hygiene
jobs:
- job: Compile
displayName: Compile & Hygiene
pool: vscode-1es-vscode-linux-20.04
variables:
VSCODE_ARCH: x64
steps:
- template: product-compile.yml
parameters:
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
- stage: Windows
dependsOn:
- Compile
pool: vscode-1es-vscode-windows-2019
jobs:
- job: WindowsUnitTests
displayName: Unit Tests
timeoutInMinutes: 120
variables:
VSCODE_ARCH: x64
steps:
- template: win32/product-build-win32.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: true
VSCODE_RUN_INTEGRATION_TESTS: false
VSCODE_RUN_SMOKE_TESTS: false
- job: WindowsIntegrationTests
displayName: Integration Tests
timeoutInMinutes: 120
variables:
VSCODE_ARCH: x64
steps:
- template: win32/product-build-win32.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: false
VSCODE_RUN_INTEGRATION_TESTS: true
VSCODE_RUN_SMOKE_TESTS: false
- job: WindowsSmokeTests
displayName: Smoke Tests
timeoutInMinutes: 120
variables:
VSCODE_ARCH: x64
steps:
- template: win32/product-build-win32.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: false
VSCODE_RUN_INTEGRATION_TESTS: false
VSCODE_RUN_SMOKE_TESTS: true
- stage: Test
dependsOn: []
jobs:
- job: Linuxx64UnitTest
displayName: Linux (Unit Tests)
pool: vscode-1es-vscode-linux-20.04
# container: vscode-bionic-x64
timeoutInMinutes: 60
variables:
VSCODE_ARCH: x64
NPM_ARCH: x64
DISPLAY: ":10"
steps:
- template: linux/product-build-linux-client.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: true
VSCODE_RUN_INTEGRATION_TESTS: false
VSCODE_RUN_SMOKE_TESTS: false
- job: Linuxx64IntegrationTest
displayName: Linux (Integration Tests)
pool: vscode-1es-vscode-linux-20.04
# container: vscode-bionic-x64
timeoutInMinutes: 60
variables:
VSCODE_ARCH: x64
NPM_ARCH: x64
DISPLAY: ":10"
steps:
- template: linux/product-build-linux-client.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: false
VSCODE_RUN_INTEGRATION_TESTS: true
VSCODE_RUN_SMOKE_TESTS: false
- job: Linuxx64SmokeTest
displayName: Linux (Smoke Tests)
pool: vscode-1es-vscode-linux-20.04
# container: vscode-bionic-x64
timeoutInMinutes: 60
variables:
VSCODE_ARCH: x64
NPM_ARCH: x64
DISPLAY: ":10"
steps:
- template: linux/product-build-linux-client.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: false
VSCODE_RUN_INTEGRATION_TESTS: false
VSCODE_RUN_SMOKE_TESTS: true
- stage: Linux
dependsOn:
- Compile
- LinuxServerDependencies
pool: vscode-1es-vscode-linux-18.04
jobs:
- job: Linuxx64UnitTest
displayName: Unit Tests
container: vscode-bionic-x64
variables:
VSCODE_ARCH: x64
NPM_ARCH: x64
DISPLAY: ":10"
steps:
- template: linux/product-build-linux-client.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: true
VSCODE_RUN_INTEGRATION_TESTS: false
VSCODE_RUN_SMOKE_TESTS: false
- job: Linuxx64IntegrationTest
displayName: Integration Tests
container: vscode-bionic-x64
variables:
VSCODE_ARCH: x64
NPM_ARCH: x64
DISPLAY: ":10"
steps:
- template: linux/product-build-linux-client.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: false
VSCODE_RUN_INTEGRATION_TESTS: true
VSCODE_RUN_SMOKE_TESTS: false
- job: Linuxx64SmokeTest
displayName: Smoke Tests
container: vscode-bionic-x64
variables:
VSCODE_ARCH: x64
NPM_ARCH: x64
DISPLAY: ":10"
steps:
- template: linux/product-build-linux-client.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: false
VSCODE_RUN_INTEGRATION_TESTS: false
VSCODE_RUN_SMOKE_TESTS: true
# - job: macOSUnitTest
# displayName: macOS (Unit Tests)
# pool:
# vmImage: macOS-latest
# timeoutInMinutes: 60
# variables:
# BUILDSECMON_OPT_IN: true
# VSCODE_ARCH: x64
# steps:
# - template: darwin/product-build-darwin.yml
# parameters:
# VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
# VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
# VSCODE_RUN_UNIT_TESTS: true
# VSCODE_RUN_INTEGRATION_TESTS: false
# VSCODE_RUN_SMOKE_TESTS: false
# - job: macOSIntegrationTest
# displayName: macOS (Integration Tests)
# pool:
# vmImage: macOS-latest
# timeoutInMinutes: 60
# variables:
# BUILDSECMON_OPT_IN: true
# VSCODE_ARCH: x64
# steps:
# - template: darwin/product-build-darwin.yml
# parameters:
# VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
# VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
# VSCODE_RUN_UNIT_TESTS: false
# VSCODE_RUN_INTEGRATION_TESTS: true
# VSCODE_RUN_SMOKE_TESTS: false
# - job: macOSSmokeTest
# displayName: macOS (Smoke Tests)
# pool:
# vmImage: macOS-latest
# timeoutInMinutes: 60
# variables:
# BUILDSECMON_OPT_IN: true
# VSCODE_ARCH: x64
# steps:
# - template: darwin/product-build-darwin.yml
# parameters:
# VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
# VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
# VSCODE_RUN_UNIT_TESTS: false
# VSCODE_RUN_INTEGRATION_TESTS: false
# VSCODE_RUN_SMOKE_TESTS: true
- stage: macOS
dependsOn:
- Compile
pool:
vmImage: macOS-latest
variables:
BUILDSECMON_OPT_IN: true
jobs:
- job: macOSUnitTest
displayName: Unit Tests
timeoutInMinutes: 90
variables:
VSCODE_ARCH: x64
steps:
- template: darwin/product-build-darwin.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: true
VSCODE_RUN_INTEGRATION_TESTS: false
VSCODE_RUN_SMOKE_TESTS: false
- job: macOSIntegrationTest
displayName: Integration Tests
timeoutInMinutes: 90
variables:
VSCODE_ARCH: x64
steps:
- template: darwin/product-build-darwin.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: false
VSCODE_RUN_INTEGRATION_TESTS: true
VSCODE_RUN_SMOKE_TESTS: false
- job: macOSSmokeTest
displayName: Smoke Tests
timeoutInMinutes: 90
variables:
VSCODE_ARCH: x64
steps:
- template: darwin/product-build-darwin.yml
parameters:
VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
VSCODE_RUN_UNIT_TESTS: false
VSCODE_RUN_INTEGRATION_TESTS: false
VSCODE_RUN_SMOKE_TESTS: true
# - job: WindowsUnitTests
# displayName: Windows (Unit Tests)
# pool: vscode-1es-vscode-windows-2019
# timeoutInMinutes: 60
# variables:
# VSCODE_ARCH: x64
# steps:
# - template: win32/product-build-win32.yml
# parameters:
# VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
# VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
# VSCODE_RUN_UNIT_TESTS: true
# VSCODE_RUN_INTEGRATION_TESTS: false
# VSCODE_RUN_SMOKE_TESTS: false
# - job: WindowsIntegrationTests
# displayName: Windows (Integration Tests)
# pool: vscode-1es-vscode-windows-2019
# timeoutInMinutes: 60
# variables:
# VSCODE_ARCH: x64
# steps:
# - template: win32/product-build-win32.yml
# parameters:
# VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
# VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
# VSCODE_RUN_UNIT_TESTS: false
# VSCODE_RUN_INTEGRATION_TESTS: true
# VSCODE_RUN_SMOKE_TESTS: false
# - job: WindowsSmokeTests
# displayName: Windows (Smoke Tests)
# pool: vscode-1es-vscode-windows-2019
# timeoutInMinutes: 60
# variables:
# VSCODE_ARCH: x64
# steps:
# - template: win32/product-build-win32.yml
# parameters:
# VSCODE_PUBLISH: ${{ variables.VSCODE_PUBLISH }}
# VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }}
# VSCODE_RUN_UNIT_TESTS: false
# VSCODE_RUN_INTEGRATION_TESTS: false
# VSCODE_RUN_SMOKE_TESTS: true

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

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

@ -16,6 +16,9 @@ const task = require('./lib/task');
const packageJson = require('../package.json');
const product = require('../product.json');
const rpmDependenciesGenerator = require('./linux/rpm/dependencies-generator');
const debianDependenciesGenerator = require('./linux/debian/dependencies-generator');
const sysrootInstaller = require('./linux/debian/install-sysroot');
const debianRecommendedDependencies = require('./linux/debian/dep-lists').recommendedDeps;
const path = require('path');
const root = path.dirname(__dirname);
const commit = util.getVersion(root);
@ -74,12 +77,16 @@ function prepareDebPackage(arch) {
let size = 0;
const control = code.pipe(es.through(
function (f) { size += f.isDirectory() ? 4096 : f.contents.length; },
function () {
async function () {
const that = this;
const sysroot = await sysrootInstaller.getSysroot(debArch);
const dependencies = debianDependenciesGenerator.getDependencies(binaryDir, product.applicationName, debArch, sysroot);
gulp.src('resources/linux/debian/control.template', { base: '.' })
.pipe(replace('@@NAME@@', product.applicationName))
.pipe(replace('@@VERSION@@', packageJson.version + '-' + linuxPackageRevision))
.pipe(replace('@@ARCHITECTURE@@', debArch))
.pipe(replace('@@DEPENDS@@', dependencies.join(', ')))
.pipe(replace('@@RECOMMENDS@@', debianRecommendedDependencies.join(', ')))
.pipe(replace('@@INSTALLEDSIZE@@', Math.ceil(size / 1024)))
.pipe(rename('DEBIAN/control'))
.pipe(es.through(function (f) { that.emit('data', f); }, function () { that.emit('end'); }));

View file

@ -188,6 +188,15 @@ class MonacoGenerator {
}
}
function generateApiProposalNames() {
let eol;
try {
const src = fs.readFileSync('src/vs/workbench/services/extensions/common/extensionsApiProposals.ts', 'utf-8');
const match = /\r?\n/m.exec(src);
eol = match ? match[0] : os.EOL;
}
catch {
eol = os.EOL;
}
const pattern = /vscode\.proposed\.([a-zA-Z]+)\.d\.ts$/;
const proposalNames = new Set();
const input = es.through();
@ -214,7 +223,7 @@ function generateApiProposalNames() {
'});',
'export type ApiProposalName = keyof typeof allApiProposals;',
'',
].join(os.EOL);
].join(eol);
this.emit('data', new File({
path: 'vs/workbench/services/extensions/common/extensionsApiProposals.ts',
contents: Buffer.from(contents)

View file

@ -226,6 +226,16 @@ class MonacoGenerator {
}
function generateApiProposalNames() {
let eol: string;
try {
const src = fs.readFileSync('src/vs/workbench/services/extensions/common/extensionsApiProposals.ts', 'utf-8');
const match = /\r?\n/m.exec(src);
eol = match ? match[0] : os.EOL;
} catch {
eol = os.EOL;
}
const pattern = /vscode\.proposed\.([a-zA-Z]+)\.d\.ts$/;
const proposalNames = new Set<string>();
@ -254,7 +264,7 @@ function generateApiProposalNames() {
'});',
'export type ApiProposalName = keyof typeof allApiProposals;',
'',
].join(os.EOL);
].join(eol);
this.emit('data', new File({
path: 'vs/workbench/services/extensions/common/extensionsApiProposals.ts',

View file

@ -89,6 +89,8 @@ function discoverAndReadFiles(ts, options) {
const in_queue = Object.create(null);
const queue = [];
const enqueue = (moduleId) => {
// To make the treeshaker work on windows...
moduleId = moduleId.replace(/\\/g, '/');
if (in_queue[moduleId]) {
return;
}

View file

@ -142,6 +142,8 @@ function discoverAndReadFiles(ts: typeof import('typescript'), options: ITreeSha
const queue: string[] = [];
const enqueue = (moduleId: string) => {
// To make the treeshaker work on windows...
moduleId = moduleId.replace(/\\/g, '/');
if (in_queue[moduleId]) {
return;
}

View file

@ -0,0 +1,156 @@
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.referenceGeneratedDepsByArch = exports.bundledDeps = exports.recommendedDeps = exports.additionalDeps = void 0;
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/additional_deps
// Additional dependencies not in the dpkg-shlibdeps output.
exports.additionalDeps = [
'ca-certificates',
'libgtk-3-0 (>= 3.9.10) | libgtk-4-1',
'libnss3 (>= 3.26)',
'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3',
'xdg-utils (>= 1.0.2)' // OS integration
];
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/manual_recommends
// Dependencies that we can only recommend
// for now since some of the older distros don't support them.
exports.recommendedDeps = [
'libvulkan1' // Move to additionalDeps once support for Trusty and Jessie are dropped.
];
// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/98.0.4758.109:chrome/installer/linux/BUILD.gn;l=64-80
// and the Linux Archive build
// Shared library dependencies that we already bundle.
exports.bundledDeps = [
'libEGL.so',
'libGLESv2.so',
'libvulkan.so.1',
'swiftshader_libEGL.so',
'swiftshader_libGLESv2.so',
'libvk_swiftshader.so',
'libffmpeg.so'
];
exports.referenceGeneratedDepsByArch = {
'amd64': [
'ca-certificates',
'libasound2 (>= 1.0.16)',
'libatk-bridge2.0-0 (>= 2.5.3)',
'libatk1.0-0 (>= 2.2.0)',
'libatspi2.0-0 (>= 2.9.90)',
'libc6 (>= 2.14)',
'libc6 (>= 2.17)',
'libc6 (>= 2.2.5)',
'libcairo2 (>= 1.6.0)',
'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3',
'libdbus-1-3 (>= 1.5.12)',
'libdrm2 (>= 2.4.38)',
'libexpat1 (>= 2.0.1)',
'libgbm1 (>= 8.1~0)',
'libgcc1 (>= 1:3.0)',
'libglib2.0-0 (>= 2.16.0)',
'libglib2.0-0 (>= 2.39.4)',
'libgtk-3-0 (>= 3.9.10)',
'libgtk-3-0 (>= 3.9.10) | libgtk-4-1',
'libnspr4 (>= 2:4.9-2~)',
'libnss3 (>= 2:3.22)',
'libnss3 (>= 3.26)',
'libpango-1.0-0 (>= 1.14.0)',
'libsecret-1-0 (>= 0.18)',
'libx11-6',
'libx11-6 (>= 2:1.4.99.1)',
'libxcb1 (>= 1.9.2)',
'libxcomposite1 (>= 1:0.4.4-1)',
'libxdamage1 (>= 1:1.1)',
'libxext6',
'libxfixes3',
'libxkbcommon0 (>= 0.4.1)',
'libxkbfile1',
'libxrandr2',
'xdg-utils (>= 1.0.2)'
],
'armhf': [
'ca-certificates',
'libasound2 (>= 1.0.16)',
'libatk-bridge2.0-0 (>= 2.5.3)',
'libatk1.0-0 (>= 2.2.0)',
'libatspi2.0-0 (>= 2.9.90)',
'libc6 (>= 2.17)',
'libc6 (>= 2.4)',
'libc6 (>= 2.9)',
'libcairo2 (>= 1.6.0)',
'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3',
'libdbus-1-3 (>= 1.5.12)',
'libdrm2 (>= 2.4.38)',
'libexpat1 (>= 2.0.1)',
'libgbm1 (>= 8.1~0)',
'libgcc1 (>= 1:3.0)',
'libgcc1 (>= 1:3.5)',
'libglib2.0-0 (>= 2.16.0)',
'libglib2.0-0 (>= 2.39.4)',
'libgtk-3-0 (>= 3.9.10)',
'libgtk-3-0 (>= 3.9.10) | libgtk-4-1',
'libnspr4 (>= 2:4.9-2~)',
'libnss3 (>= 2:3.22)',
'libnss3 (>= 3.26)',
'libpango-1.0-0 (>= 1.14.0)',
'libsecret-1-0 (>= 0.18)',
'libstdc++6 (>= 4.1.1)',
'libstdc++6 (>= 5)',
'libstdc++6 (>= 5.2)',
'libstdc++6 (>= 6)',
'libx11-6',
'libx11-6 (>= 2:1.4.99.1)',
'libxcb1 (>= 1.9.2)',
'libxcomposite1 (>= 1:0.4.4-1)',
'libxdamage1 (>= 1:1.1)',
'libxext6',
'libxfixes3',
'libxkbcommon0 (>= 0.4.1)',
'libxkbfile1',
'libxrandr2',
'xdg-utils (>= 1.0.2)'
],
'arm64': [
'ca-certificates',
'libasound2 (>= 1.0.16)',
'libatk-bridge2.0-0 (>= 2.5.3)',
'libatk1.0-0 (>= 2.2.0)',
'libatspi2.0-0 (>= 2.9.90)',
'libc6 (>= 2.17)',
'libcairo2 (>= 1.6.0)',
'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3',
'libdbus-1-3 (>= 1.0.2)',
'libdrm2 (>= 2.4.38)',
'libexpat1 (>= 2.0.1)',
'libgbm1 (>= 8.1~0)',
'libgcc1 (>= 1:3.0)',
'libgcc1 (>= 1:4.2)',
'libgcc1 (>= 1:4.5)',
'libglib2.0-0 (>= 2.16.0)',
'libglib2.0-0 (>= 2.39.4)',
'libgtk-3-0 (>= 3.9.10)',
'libgtk-3-0 (>= 3.9.10) | libgtk-4-1',
'libnspr4 (>= 2:4.9-2~)',
'libnss3 (>= 2:3.22)',
'libnss3 (>= 3.26)',
'libpango-1.0-0 (>= 1.14.0)',
'libsecret-1-0 (>= 0.18)',
'libstdc++6 (>= 4.1.1)',
'libstdc++6 (>= 5)',
'libstdc++6 (>= 5.2)',
'libstdc++6 (>= 6)',
'libx11-6',
'libx11-6 (>= 2:1.4.99.1)',
'libxcb1 (>= 1.9.2)',
'libxcomposite1 (>= 1:0.4.4-1)',
'libxdamage1 (>= 1:1.1)',
'libxext6',
'libxfixes3',
'libxkbcommon0 (>= 0.4.1)',
'libxkbfile1',
'libxrandr2',
'xdg-utils (>= 1.0.2)'
]
};

View file

@ -0,0 +1,157 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/additional_deps
// Additional dependencies not in the dpkg-shlibdeps output.
export const additionalDeps = [
'ca-certificates', // Make sure users have SSL certificates.
'libgtk-3-0 (>= 3.9.10) | libgtk-4-1',
'libnss3 (>= 3.26)',
'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', // For Breakpad crash reports.
'xdg-utils (>= 1.0.2)' // OS integration
];
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/manual_recommends
// Dependencies that we can only recommend
// for now since some of the older distros don't support them.
export const recommendedDeps = [
'libvulkan1' // Move to additionalDeps once support for Trusty and Jessie are dropped.
];
// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/98.0.4758.109:chrome/installer/linux/BUILD.gn;l=64-80
// and the Linux Archive build
// Shared library dependencies that we already bundle.
export const bundledDeps = [
'libEGL.so',
'libGLESv2.so',
'libvulkan.so.1',
'swiftshader_libEGL.so',
'swiftshader_libGLESv2.so',
'libvk_swiftshader.so',
'libffmpeg.so'
];
export const referenceGeneratedDepsByArch = {
'amd64': [
'ca-certificates',
'libasound2 (>= 1.0.16)',
'libatk-bridge2.0-0 (>= 2.5.3)',
'libatk1.0-0 (>= 2.2.0)',
'libatspi2.0-0 (>= 2.9.90)',
'libc6 (>= 2.14)',
'libc6 (>= 2.17)',
'libc6 (>= 2.2.5)',
'libcairo2 (>= 1.6.0)',
'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3',
'libdbus-1-3 (>= 1.5.12)',
'libdrm2 (>= 2.4.38)',
'libexpat1 (>= 2.0.1)',
'libgbm1 (>= 8.1~0)',
'libgcc1 (>= 1:3.0)',
'libglib2.0-0 (>= 2.16.0)',
'libglib2.0-0 (>= 2.39.4)',
'libgtk-3-0 (>= 3.9.10)',
'libgtk-3-0 (>= 3.9.10) | libgtk-4-1',
'libnspr4 (>= 2:4.9-2~)',
'libnss3 (>= 2:3.22)',
'libnss3 (>= 3.26)',
'libpango-1.0-0 (>= 1.14.0)',
'libsecret-1-0 (>= 0.18)',
'libx11-6',
'libx11-6 (>= 2:1.4.99.1)',
'libxcb1 (>= 1.9.2)',
'libxcomposite1 (>= 1:0.4.4-1)',
'libxdamage1 (>= 1:1.1)',
'libxext6',
'libxfixes3',
'libxkbcommon0 (>= 0.4.1)',
'libxkbfile1',
'libxrandr2',
'xdg-utils (>= 1.0.2)'
],
'armhf': [
'ca-certificates',
'libasound2 (>= 1.0.16)',
'libatk-bridge2.0-0 (>= 2.5.3)',
'libatk1.0-0 (>= 2.2.0)',
'libatspi2.0-0 (>= 2.9.90)',
'libc6 (>= 2.17)',
'libc6 (>= 2.4)',
'libc6 (>= 2.9)',
'libcairo2 (>= 1.6.0)',
'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3',
'libdbus-1-3 (>= 1.5.12)',
'libdrm2 (>= 2.4.38)',
'libexpat1 (>= 2.0.1)',
'libgbm1 (>= 8.1~0)',
'libgcc1 (>= 1:3.0)',
'libgcc1 (>= 1:3.5)',
'libglib2.0-0 (>= 2.16.0)',
'libglib2.0-0 (>= 2.39.4)',
'libgtk-3-0 (>= 3.9.10)',
'libgtk-3-0 (>= 3.9.10) | libgtk-4-1',
'libnspr4 (>= 2:4.9-2~)',
'libnss3 (>= 2:3.22)',
'libnss3 (>= 3.26)',
'libpango-1.0-0 (>= 1.14.0)',
'libsecret-1-0 (>= 0.18)',
'libstdc++6 (>= 4.1.1)',
'libstdc++6 (>= 5)',
'libstdc++6 (>= 5.2)',
'libstdc++6 (>= 6)',
'libx11-6',
'libx11-6 (>= 2:1.4.99.1)',
'libxcb1 (>= 1.9.2)',
'libxcomposite1 (>= 1:0.4.4-1)',
'libxdamage1 (>= 1:1.1)',
'libxext6',
'libxfixes3',
'libxkbcommon0 (>= 0.4.1)',
'libxkbfile1',
'libxrandr2',
'xdg-utils (>= 1.0.2)'
],
'arm64': [
'ca-certificates',
'libasound2 (>= 1.0.16)',
'libatk-bridge2.0-0 (>= 2.5.3)',
'libatk1.0-0 (>= 2.2.0)',
'libatspi2.0-0 (>= 2.9.90)',
'libc6 (>= 2.17)',
'libcairo2 (>= 1.6.0)',
'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3',
'libdbus-1-3 (>= 1.0.2)',
'libdrm2 (>= 2.4.38)',
'libexpat1 (>= 2.0.1)',
'libgbm1 (>= 8.1~0)',
'libgcc1 (>= 1:3.0)',
'libgcc1 (>= 1:4.2)',
'libgcc1 (>= 1:4.5)',
'libglib2.0-0 (>= 2.16.0)',
'libglib2.0-0 (>= 2.39.4)',
'libgtk-3-0 (>= 3.9.10)',
'libgtk-3-0 (>= 3.9.10) | libgtk-4-1',
'libnspr4 (>= 2:4.9-2~)',
'libnss3 (>= 2:3.22)',
'libnss3 (>= 3.26)',
'libpango-1.0-0 (>= 1.14.0)',
'libsecret-1-0 (>= 0.18)',
'libstdc++6 (>= 4.1.1)',
'libstdc++6 (>= 5)',
'libstdc++6 (>= 5.2)',
'libstdc++6 (>= 6)',
'libx11-6',
'libx11-6 (>= 2:1.4.99.1)',
'libxcb1 (>= 1.9.2)',
'libxcomposite1 (>= 1:0.4.4-1)',
'libxdamage1 (>= 1:1.1)',
'libxext6',
'libxfixes3',
'libxkbcommon0 (>= 0.4.1)',
'libxkbfile1',
'libxrandr2',
'xdg-utils (>= 1.0.2)'
]
};

View file

@ -0,0 +1,129 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDependencies = void 0;
const child_process_1 = require("child_process");
const fs_1 = require("fs");
const os_1 = require("os");
const path = require("path");
const dep_lists_1 = require("./dep-lists");
// A flag that can easily be toggled.
// Make sure to compile the build directory after toggling the value.
// If false, we warn about new dependencies if they show up
// while running the Debian prepare package task for a release.
// If true, we fail the build if there are new dependencies found during that task.
// The reference dependencies, which one has to update when the new dependencies
// are valid, are in dep-lists.ts
const FAIL_BUILD_FOR_NEW_DEPENDENCIES = true;
function getDependencies(buildDir, applicationName, arch, sysroot) {
// Get the files for which we want to find dependencies.
const nativeModulesPath = path.join(buildDir, 'resources', 'app', 'node_modules.asar.unpacked');
const findResult = (0, child_process_1.spawnSync)('find', [nativeModulesPath, '-name', '*.node']);
if (findResult.status) {
console.error('Error finding files:');
console.error(findResult.stderr.toString());
return [];
}
const files = findResult.stdout.toString().trimEnd().split('\n');
const appPath = path.join(buildDir, applicationName);
files.push(appPath);
// Add chrome sandbox and crashpad handler.
files.push(path.join(buildDir, 'chrome-sandbox'));
files.push(path.join(buildDir, 'chrome_crashpad_handler'));
// Generate the dependencies.
const dependencies = files.map((file) => calculatePackageDeps(file, arch, sysroot));
// Add additional dependencies.
const additionalDepsSet = new Set(dep_lists_1.additionalDeps);
dependencies.push(additionalDepsSet);
// Merge all the dependencies.
const mergedDependencies = mergePackageDeps(dependencies);
let sortedDependencies = [];
for (const dependency of mergedDependencies) {
sortedDependencies.push(dependency);
}
sortedDependencies.sort();
// Exclude bundled dependencies
sortedDependencies = sortedDependencies.filter(dependency => {
return !dep_lists_1.bundledDeps.some(bundledDep => dependency.startsWith(bundledDep));
});
const referenceGeneratedDeps = dep_lists_1.referenceGeneratedDepsByArch[arch];
if (JSON.stringify(sortedDependencies) !== JSON.stringify(referenceGeneratedDeps)) {
const failMessage = 'The dependencies list has changed.'
+ '\nOld:\n' + referenceGeneratedDeps.join('\n')
+ '\nNew:\n' + sortedDependencies.join('\n');
if (FAIL_BUILD_FOR_NEW_DEPENDENCIES) {
throw new Error(failMessage);
}
else {
console.warn(failMessage);
}
}
return sortedDependencies;
}
exports.getDependencies = getDependencies;
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/calculate_package_deps.py.
function calculatePackageDeps(binaryPath, arch, sysroot) {
try {
if (!((0, fs_1.statSync)(binaryPath).mode & fs_1.constants.S_IXUSR)) {
throw new Error(`Binary ${binaryPath} needs to have an executable bit set.`);
}
}
catch (e) {
// The package might not exist. Don't re-throw the error here.
console.error('Tried to stat ' + binaryPath + ' but failed.');
}
// Get the Chromium dpkg-shlibdeps file.
const dpkgShlibdepsUrl = 'https://raw.githubusercontent.com/chromium/chromium/100.0.4896.160/third_party/dpkg-shlibdeps/dpkg-shlibdeps.pl';
const dpkgShlibdepsScriptLocation = `${(0, os_1.tmpdir)()}/dpkg-shlibdeps.pl`;
const result = (0, child_process_1.spawnSync)('curl', [dpkgShlibdepsUrl, '-o', dpkgShlibdepsScriptLocation]);
if (result.status !== 0) {
throw new Error('Cannot retrieve dpkg-shlibdeps. Stderr:\n' + result.stderr);
}
const cmd = [dpkgShlibdepsScriptLocation, '--ignore-weak-undefined'];
switch (arch) {
case 'amd64':
cmd.push(`-l${sysroot}/usr/lib/x86_64-linux-gnu`, `-l${sysroot}/lib/x86_64-linux-gnu`);
break;
case 'armhf':
cmd.push(`-l${sysroot}/usr/lib/arm-linux-gnueabihf`, `-l${sysroot}/lib/arm-linux-gnueabihf`);
break;
case 'arm64':
cmd.push(`-l${sysroot}/usr/lib/aarch64-linux-gnu`, `-l${sysroot}/lib/aarch64-linux-gnu`);
break;
default:
throw new Error('Unsupported architecture ' + arch);
}
cmd.push(`-l${sysroot}/usr/lib`);
cmd.push('-O', '-e', path.resolve(binaryPath));
const dpkgShlibdepsResult = (0, child_process_1.spawnSync)('perl', cmd, { cwd: sysroot });
if (dpkgShlibdepsResult.status !== 0) {
throw new Error(`dpkg-shlibdeps failed with exit code ${dpkgShlibdepsResult.status}. stderr:\n${dpkgShlibdepsResult.stderr} `);
}
const shlibsDependsPrefix = 'shlibs:Depends=';
const requiresList = dpkgShlibdepsResult.stdout.toString('utf-8').trimEnd().split('\n');
let depsStr = '';
for (const line of requiresList) {
if (line.startsWith(shlibsDependsPrefix)) {
depsStr = line.substring(shlibsDependsPrefix.length);
}
}
const requires = new Set(depsStr.split(', ').sort());
return requires;
}
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py.
function mergePackageDeps(inputDeps) {
// For now, see if directly appending the dependencies helps.
const requires = new Set();
for (const depSet of inputDeps) {
for (const dep of depSet) {
const trimmedDependency = dep.trim();
if (trimmedDependency.length && !trimmedDependency.startsWith('#')) {
requires.add(trimmedDependency);
}
}
}
return requires;
}

View file

@ -0,0 +1,145 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { spawnSync } from 'child_process';
import { constants, statSync } from 'fs';
import { tmpdir } from 'os';
import path = require('path');
import { additionalDeps, bundledDeps, referenceGeneratedDepsByArch } from './dep-lists';
import { ArchString } from './types';
// A flag that can easily be toggled.
// Make sure to compile the build directory after toggling the value.
// If false, we warn about new dependencies if they show up
// while running the Debian prepare package task for a release.
// If true, we fail the build if there are new dependencies found during that task.
// The reference dependencies, which one has to update when the new dependencies
// are valid, are in dep-lists.ts
const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true;
export function getDependencies(buildDir: string, applicationName: string, arch: ArchString, sysroot: string): string[] {
// Get the files for which we want to find dependencies.
const nativeModulesPath = path.join(buildDir, 'resources', 'app', 'node_modules.asar.unpacked');
const findResult = spawnSync('find', [nativeModulesPath, '-name', '*.node']);
if (findResult.status) {
console.error('Error finding files:');
console.error(findResult.stderr.toString());
return [];
}
const files = findResult.stdout.toString().trimEnd().split('\n');
const appPath = path.join(buildDir, applicationName);
files.push(appPath);
// Add chrome sandbox and crashpad handler.
files.push(path.join(buildDir, 'chrome-sandbox'));
files.push(path.join(buildDir, 'chrome_crashpad_handler'));
// Generate the dependencies.
const dependencies: Set<string>[] = files.map((file) => calculatePackageDeps(file, arch, sysroot));
// Add additional dependencies.
const additionalDepsSet = new Set(additionalDeps);
dependencies.push(additionalDepsSet);
// Merge all the dependencies.
const mergedDependencies = mergePackageDeps(dependencies);
let sortedDependencies: string[] = [];
for (const dependency of mergedDependencies) {
sortedDependencies.push(dependency);
}
sortedDependencies.sort();
// Exclude bundled dependencies
sortedDependencies = sortedDependencies.filter(dependency => {
return !bundledDeps.some(bundledDep => dependency.startsWith(bundledDep));
});
const referenceGeneratedDeps = referenceGeneratedDepsByArch[arch];
if (JSON.stringify(sortedDependencies) !== JSON.stringify(referenceGeneratedDeps)) {
const failMessage = 'The dependencies list has changed.'
+ '\nOld:\n' + referenceGeneratedDeps.join('\n')
+ '\nNew:\n' + sortedDependencies.join('\n');
if (FAIL_BUILD_FOR_NEW_DEPENDENCIES) {
throw new Error(failMessage);
} else {
console.warn(failMessage);
}
}
return sortedDependencies;
}
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/calculate_package_deps.py.
function calculatePackageDeps(binaryPath: string, arch: ArchString, sysroot: string): Set<string> {
try {
if (!(statSync(binaryPath).mode & constants.S_IXUSR)) {
throw new Error(`Binary ${binaryPath} needs to have an executable bit set.`);
}
} catch (e) {
// The package might not exist. Don't re-throw the error here.
console.error('Tried to stat ' + binaryPath + ' but failed.');
}
// Get the Chromium dpkg-shlibdeps file.
const dpkgShlibdepsUrl = 'https://raw.githubusercontent.com/chromium/chromium/100.0.4896.160/third_party/dpkg-shlibdeps/dpkg-shlibdeps.pl';
const dpkgShlibdepsScriptLocation = `${tmpdir()}/dpkg-shlibdeps.pl`;
const result = spawnSync('curl', [dpkgShlibdepsUrl, '-o', dpkgShlibdepsScriptLocation]);
if (result.status !== 0) {
throw new Error('Cannot retrieve dpkg-shlibdeps. Stderr:\n' + result.stderr);
}
const cmd = [dpkgShlibdepsScriptLocation, '--ignore-weak-undefined'];
switch (arch) {
case 'amd64':
cmd.push(`-l${sysroot}/usr/lib/x86_64-linux-gnu`,
`-l${sysroot}/lib/x86_64-linux-gnu`);
break;
case 'armhf':
cmd.push(`-l${sysroot}/usr/lib/arm-linux-gnueabihf`,
`-l${sysroot}/lib/arm-linux-gnueabihf`);
break;
case 'arm64':
cmd.push(`-l${sysroot}/usr/lib/aarch64-linux-gnu`,
`-l${sysroot}/lib/aarch64-linux-gnu`);
break;
default:
throw new Error('Unsupported architecture ' + arch);
}
cmd.push(`-l${sysroot}/usr/lib`);
cmd.push('-O', '-e', path.resolve(binaryPath));
const dpkgShlibdepsResult = spawnSync('perl', cmd, { cwd: sysroot });
if (dpkgShlibdepsResult.status !== 0) {
throw new Error(`dpkg-shlibdeps failed with exit code ${dpkgShlibdepsResult.status}. stderr:\n${dpkgShlibdepsResult.stderr} `);
}
const shlibsDependsPrefix = 'shlibs:Depends=';
const requiresList = dpkgShlibdepsResult.stdout.toString('utf-8').trimEnd().split('\n');
let depsStr = '';
for (const line of requiresList) {
if (line.startsWith(shlibsDependsPrefix)) {
depsStr = line.substring(shlibsDependsPrefix.length);
}
}
const requires = new Set(depsStr.split(', ').sort());
return requires;
}
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py.
function mergePackageDeps(inputDeps: Set<string>[]): Set<string> {
// For now, see if directly appending the dependencies helps.
const requires = new Set<string>();
for (const depSet of inputDeps) {
for (const dep of depSet) {
const trimmedDependency = dep.trim();
if (trimmedDependency.length && !trimmedDependency.startsWith('#')) {
requires.add(trimmedDependency);
}
}
}
return requires;
}

View file

@ -0,0 +1,81 @@
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSysroot = void 0;
const child_process_1 = require("child_process");
const crypto_1 = require("crypto");
const os_1 = require("os");
const fs = require("fs");
const https = require("https");
const path = require("path");
const sysroots_1 = require("./sysroots");
// Based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/install-sysroot.py.
const URL_PREFIX = 'https://msftelectron.blob.core.windows.net';
const URL_PATH = 'sysroots/toolchain';
function getSha(filename) {
const hash = (0, crypto_1.createHash)('sha1');
// Read file 1 MB at a time
const fd = fs.openSync(filename, 'r');
const buffer = Buffer.alloc(1024 * 1024);
let position = 0;
let bytesRead = 0;
while ((bytesRead = fs.readSync(fd, buffer, 0, buffer.length, position)) === buffer.length) {
hash.update(buffer);
position += bytesRead;
}
hash.update(buffer.slice(0, bytesRead));
return hash.digest('hex');
}
async function getSysroot(arch) {
const sysrootDict = sysroots_1.sysrootInfo[arch];
const tarballFilename = sysrootDict['Tarball'];
const tarballSha = sysrootDict['Sha1Sum'];
const sysroot = path.join((0, os_1.tmpdir)(), sysrootDict['SysrootDir']);
const url = [URL_PREFIX, URL_PATH, tarballSha, tarballFilename].join('/');
const stamp = path.join(sysroot, '.stamp');
if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === url) {
return sysroot;
}
console.log(`Installing Debian ${arch} root image: ${sysroot}`);
fs.rmSync(sysroot, { recursive: true, force: true });
fs.mkdirSync(sysroot);
const tarball = path.join(sysroot, tarballFilename);
console.log(`Downloading ${url}`);
let downloadSuccess = false;
for (let i = 0; i < 3 && !downloadSuccess; i++) {
await new Promise((c) => {
https.get(url, (res) => {
const chunks = [];
res.on('data', (chunk) => {
chunks.push(chunk);
});
res.on('end', () => {
fs.writeFileSync(tarball, Buffer.concat(chunks));
downloadSuccess = true;
c();
});
}).on('error', (err) => {
console.error('Encountered an error during the download attempt: ' + err.message);
c();
});
});
}
if (!downloadSuccess) {
throw new Error('Failed to download ' + url);
}
const sha = getSha(tarball);
if (sha !== tarballSha) {
throw new Error(`Tarball sha1sum is wrong. Expected ${tarballSha}, actual ${sha}`);
}
const proc = (0, child_process_1.spawnSync)('tar', ['xf', tarball, '-C', sysroot]);
if (proc.status) {
throw new Error('Tarball extraction failed with code ' + proc.status);
}
fs.rmSync(tarball);
fs.writeFileSync(stamp, url);
return sysroot;
}
exports.getSysroot = getSysroot;

View file

@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { spawnSync } from 'child_process';
import { createHash } from 'crypto';
import { tmpdir } from 'os';
import * as fs from 'fs';
import * as https from 'https';
import * as path from 'path';
import { sysrootInfo } from './sysroots';
import { ArchString } from './types';
// Based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/install-sysroot.py.
const URL_PREFIX = 'https://msftelectron.blob.core.windows.net';
const URL_PATH = 'sysroots/toolchain';
function getSha(filename: fs.PathLike): string {
const hash = createHash('sha1');
// Read file 1 MB at a time
const fd = fs.openSync(filename, 'r');
const buffer = Buffer.alloc(1024 * 1024);
let position = 0;
let bytesRead = 0;
while ((bytesRead = fs.readSync(fd, buffer, 0, buffer.length, position)) === buffer.length) {
hash.update(buffer);
position += bytesRead;
}
hash.update(buffer.slice(0, bytesRead));
return hash.digest('hex');
}
type SysrootDictEntry = {
Sha1Sum: string;
SysrootDir: string;
Tarball: string;
};
export async function getSysroot(arch: ArchString): Promise<string> {
const sysrootDict: SysrootDictEntry = sysrootInfo[arch];
const tarballFilename = sysrootDict['Tarball'];
const tarballSha = sysrootDict['Sha1Sum'];
const sysroot = path.join(tmpdir(), sysrootDict['SysrootDir']);
const url = [URL_PREFIX, URL_PATH, tarballSha, tarballFilename].join('/');
const stamp = path.join(sysroot, '.stamp');
if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === url) {
return sysroot;
}
console.log(`Installing Debian ${arch} root image: ${sysroot}`);
fs.rmSync(sysroot, { recursive: true, force: true });
fs.mkdirSync(sysroot);
const tarball = path.join(sysroot, tarballFilename);
console.log(`Downloading ${url}`);
let downloadSuccess = false;
for (let i = 0; i < 3 && !downloadSuccess; i++) {
await new Promise<void>((c) => {
https.get(url, (res) => {
const chunks: Uint8Array[] = [];
res.on('data', (chunk) => {
chunks.push(chunk);
});
res.on('end', () => {
fs.writeFileSync(tarball, Buffer.concat(chunks));
downloadSuccess = true;
c();
});
}).on('error', (err) => {
console.error('Encountered an error during the download attempt: ' + err.message);
c();
});
});
}
if (!downloadSuccess) {
throw new Error('Failed to download ' + url);
}
const sha = getSha(tarball);
if (sha !== tarballSha) {
throw new Error(`Tarball sha1sum is wrong. Expected ${tarballSha}, actual ${sha}`);
}
const proc = spawnSync('tar', ['xf', tarball, '-C', sysroot]);
if (proc.status) {
throw new Error('Tarball extraction failed with code ' + proc.status);
}
fs.rmSync(tarball);
fs.writeFileSync(stamp, url);
return sysroot;
}

View file

@ -0,0 +1,26 @@
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.sysrootInfo = void 0;
// Based on https://github.com/electron/electron/blob/main/script/sysroots.json,
// which itself is based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/sysroots.json.
exports.sysrootInfo = {
'amd64': {
'Sha1Sum': '7e008cea9eae822d80d55c67fbb5ef4204678e74',
'SysrootDir': 'debian_sid_amd64-sysroot',
'Tarball': 'debian_sid_amd64_sysroot.tar.xz'
},
'armhf': {
'Sha1Sum': 'b6f4bb07817bea91b06514a9c1e3832df5a90dbf',
'SysrootDir': 'debian_sid_arm-sysroot',
'Tarball': 'debian_sid_arm_sysroot.tar.xz'
},
'arm64': {
'Sha1Sum': '5a56c1ef714154ea5003bcafb16f21b0f8dde023',
'SysrootDir': 'debian_sid_arm64-sysroot',
'Tarball': 'debian_sid_arm64_sysroot.tar.xz'
}
};

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.
*--------------------------------------------------------------------------------------------*/
// Based on https://github.com/electron/electron/blob/main/script/sysroots.json,
// which itself is based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/sysroots.json.
export const sysrootInfo = {
'amd64': {
'Sha1Sum': '7e008cea9eae822d80d55c67fbb5ef4204678e74',
'SysrootDir': 'debian_sid_amd64-sysroot',
'Tarball': 'debian_sid_amd64_sysroot.tar.xz'
},
'armhf': {
'Sha1Sum': 'b6f4bb07817bea91b06514a9c1e3832df5a90dbf',
'SysrootDir': 'debian_sid_arm-sysroot',
'Tarball': 'debian_sid_arm_sysroot.tar.xz'
},
'arm64': {
'Sha1Sum': '5a56c1ef714154ea5003bcafb16f21b0f8dde023',
'SysrootDir': 'debian_sid_arm64-sysroot',
'Tarball': 'debian_sid_arm64_sysroot.tar.xz'
}
};

View file

@ -0,0 +1,6 @@
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });

View file

@ -0,0 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export type ArchString = 'amd64' | 'armhf' | 'arm64';

View file

@ -81,7 +81,7 @@ function calculatePackageDeps(binaryPath) {
const requires = new Set(findRequiresResult.stdout.toString('utf-8').trimEnd().split('\n'));
return requires;
}
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py.
function mergePackageDeps(inputDeps) {
const requires = new Set();
for (const depSet of inputDeps) {

View file

@ -92,7 +92,7 @@ function calculatePackageDeps(binaryPath: string): Set<string> {
return requires;
}
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py
// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py.
function mergePackageDeps(inputDeps: Set<string>[]): Set<string> {
const requires = new Set<string>();
for (const depSet of inputDeps) {

View file

@ -1494,7 +1494,7 @@ begin
Permissions := '/grant:r "*S-1-5-18:(OI)(CI)F" /grant:r "*S-1-5-32-544:(OI)(CI)F" /grant:r "*S-1-5-11:(OI)(CI)RX" /grant:r "*S-1-5-32-545:(OI)(CI)RX"';
#if "user" == InstallTarget
Permissions := Permissions + ' /grant:r "*S-1-3-0:(OI)(CI)F"';
Permissions := Permissions + Format(' /grant:r "*S-1-3-0:(OI)(CI)F" /grant:r "%s:(OI)(CI)F"', [GetUserNameString()]);
#endif
Exec(ExpandConstant('{sys}\icacls.exe'), ExpandConstant('"{app}" /inheritancelevel:r ') + Permissions, '', SW_HIDE, ewWaitUntilTerminated, ResultCode);

View file

@ -40,8 +40,8 @@ function registerVariableCompletions(pattern: string): vscode.Disposable {
provideCompletionItems(document, position, _token) {
const location = getLocation(document.getText(), document.offsetAt(position));
if (isCompletingInsidePropertyStringValue(document, location, position)) {
let range = document.getWordRangeAtPosition(position, /\$\{[^\}]*\}/);
if (!range || range.end.isEqual(position) || range.start.isEqual(position)) {
let range = document.getWordRangeAtPosition(position, /\$\{[^"\}]*\}?/);
if (!range || range.start.isEqual(position) || range.end.isEqual(position) && document.getText(range).endsWith('}')) {
range = new vscode.Range(position, position);
}

View file

@ -96,8 +96,8 @@ export class SettingsDocument {
return completions;
}
let range = this.document.getWordRangeAtPosition(pos, /\$\{[^\}]*\}/);
if (!range || range.end.isEqual(pos) || range.start.isEqual(pos)) {
let range = this.document.getWordRangeAtPosition(pos, /\$\{[^"\}]*\}?/);
if (!range || range.start.isEqual(pos) || range.end.isEqual(pos) && this.document.getText(range).endsWith('}')) {
range = new vscode.Range(pos, pos);
}

View file

@ -73,6 +73,20 @@ suite('Completions in settings.json', () => {
const expected = { label: '${activeEditorMedium}', resultText };
await testCompletion(testFile, 'jsonc', content, expected);
}
{ // replacing a partial variable
const content = [
'{',
' "window.title": "${a|"',
'}',
].join('\n');
const resultText = [
'{',
' "window.title": "${dirty}"',
'}',
].join('\n');
const expected = { label: '${dirty}', resultText };
await testCompletion(testFile, 'jsonc', content, expected);
}
{ // inserting a literal
const content = [
'{',
@ -382,6 +396,32 @@ suite('Completions in launch.json', () => {
const expected = { label: '${cwd}', resultText };
await testCompletion(testFile, 'jsonc', content, expected);
}
{
const content = [
'{',
' "version": "0.2.0",',
' "configurations": [',
' {',
' "name": "Do It",',
' "program": "${workspace|"',
' }',
' ]',
'}',
].join('\n');
const resultText = [
'{',
' "version": "0.2.0",',
' "configurations": [',
' {',
' "name": "Do It",',
' "program": "${cwd}"',
' }',
' ]',
'}',
].join('\n');
const expected = { label: '${cwd}', resultText };
await testCompletion(testFile, 'jsonc', content, expected);
}
});
});

View file

@ -601,7 +601,7 @@
"command": "git.acceptMerge",
"title": "%command.git.acceptMerge%",
"category": "Git",
"enablement": "isMergeEditor"
"enablement": "isMergeEditor && mergeEditorResultUri in git.mergeChanges"
}
],
"keybindings": [
@ -1549,7 +1549,7 @@
"merge/toolbar": [
{
"command": "git.acceptMerge",
"when": "isMergeEditor && baseResourceScheme =~ /^git$|^file$/"
"when": "isMergeEditor && mergeEditorBaseUri =~ /^(git|file):/ && mergeEditorResultUri in git.mergeChanges"
}
],
"scm/change/title": [
@ -2522,7 +2522,7 @@
},
"git.mergeEditor": {
"type": "boolean",
"default": false,
"default": true,
"markdownDescription": "%config.mergeEditor%",
"scope": "window"
}

View file

@ -17,6 +17,7 @@ interface ActionButtonState {
readonly HEAD: Branch | undefined;
readonly isCommitInProgress: boolean;
readonly isMergeInProgress: boolean;
readonly isRebaseInProgress: boolean;
readonly isSyncInProgress: boolean;
readonly repositoryHasChangesToCommit: boolean;
}
@ -43,6 +44,7 @@ export class ActionButtonCommand {
HEAD: undefined,
isCommitInProgress: false,
isMergeInProgress: false,
isRebaseInProgress: false,
isSyncInProgress: false,
repositoryHasChangesToCommit: false
};
@ -70,7 +72,7 @@ export class ActionButtonCommand {
}
get button(): SourceControlActionButton | undefined {
if (!this.state.HEAD || !this.state.HEAD.name) { return undefined; }
if (!this.state.HEAD) { return undefined; }
let actionButton: SourceControlActionButton | undefined;
@ -90,7 +92,25 @@ export class ActionButtonCommand {
// The button is disabled
if (!showActionButton.commit) { return undefined; }
let title: string, tooltip: string, commandArg: string;
return {
command: this.getCommitActionButtonPrimaryCommand(),
secondaryCommands: this.getCommitActionButtonSecondaryCommands(),
enabled: this.state.repositoryHasChangesToCommit && !this.state.isCommitInProgress && !this.state.isMergeInProgress
};
}
private getCommitActionButtonPrimaryCommand(): Command {
let commandArg = '';
let title = localize('scm button commit title', "{0} Commit", '$(check)');
let tooltip = this.state.isCommitInProgress ? localize('scm button committing tooltip', "Committing Changes...") : localize('scm button commit tooltip', "Commit Changes");
// Rebase Continue
if (this.state.isRebaseInProgress) {
return { command: 'git.commit', title, tooltip, arguments: [this.repository.sourceControl, commandArg] };
}
// Commit
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const postCommitCommand = config.get<string>('postCommitCommand');
// Branch protection
@ -133,49 +153,36 @@ export class ActionButtonCommand {
break;
}
default: {
commandArg = '';
title = localize('scm button commit title', "{0} Commit", icon ?? '$(check)');
if (alwaysCommitToNewBranch) {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing to new branch tooltip', "Committing Changes to New Branch...") :
localize('scm button commit to new branch tooltip', "Commit Changes to New Branch");
} else {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing tooltip', "Committing Changes...") :
localize('scm button commit tooltip', "Commit Changes");
}
break;
}
}
return {
command: {
command: 'git.commit',
title: title,
tooltip: tooltip,
arguments: [this.repository.sourceControl, commandArg],
},
secondaryCommands: this.getCommitActionButtonSecondaryCommands(),
enabled: this.state.repositoryHasChangesToCommit && !this.state.isCommitInProgress && !this.state.isMergeInProgress
};
return { command: 'git.commit', title, tooltip, arguments: [this.repository.sourceControl, commandArg] };
}
private getCommitActionButtonSecondaryCommands(): Command[][] {
const commandGroups: Command[][] = [];
for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) {
const commands = provider.getCommands(new ApiRepository(this.repository));
commandGroups.push((commands ?? []).map(c => {
return {
command: 'git.commit',
title: c.title,
arguments: [this.repository.sourceControl, c.command]
};
}));
}
if (!this.state.isRebaseInProgress) {
for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) {
const commands = provider.getCommands(new ApiRepository(this.repository));
commandGroups.push((commands ?? []).map(c => {
return {
command: 'git.commit',
title: c.title,
arguments: [this.repository.sourceControl, c.command]
};
}));
}
if (commandGroups.length > 0) {
commandGroups[0].splice(0, 0, { command: 'git.commit', title: localize('scm secondary button commit', "Commit") });
if (commandGroups.length > 0) {
commandGroups[0].splice(0, 0, { command: 'git.commit', title: localize('scm secondary button commit', "Commit") });
}
}
return commandGroups;
@ -185,8 +192,8 @@ export class ActionButtonCommand {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true });
// Branch does have an upstream, commit/merge is in progress, or the button is disabled
if (this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || !showActionButton.publish) { return undefined; }
// Branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled
if (this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.publish) { return undefined; }
return {
command: {
@ -206,8 +213,8 @@ export class ActionButtonCommand {
const showActionButton = config.get<{ sync: boolean }>('showActionButton', { sync: true });
const branchIsAheadOrBehind = (this.state.HEAD?.behind ?? 0) > 0 || (this.state.HEAD?.ahead ?? 0) > 0;
// Branch does not have an upstream, branch is not ahead/behind the remote branch, commit/merge is in progress, or the button is disabled
if (!this.state.HEAD?.upstream || !branchIsAheadOrBehind || this.state.isCommitInProgress || this.state.isMergeInProgress || !showActionButton.sync) { return undefined; }
// Branch does not have an upstream, branch is not ahead/behind the remote branch, commit/merge/rebase is in progress, or the button is disabled
if (!this.state.HEAD?.upstream || !branchIsAheadOrBehind || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.sync) { return undefined; }
const ahead = this.state.HEAD.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : '';
const behind = this.state.HEAD.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : '';
@ -229,7 +236,8 @@ export class ActionButtonCommand {
private onDidChangeOperations(): void {
const isCommitInProgress =
this.repository.operations.isRunning(Operation.Commit);
this.repository.operations.isRunning(Operation.Commit) ||
this.repository.operations.isRunning(Operation.RebaseContinue);
const isSyncInProgress =
this.repository.operations.isRunning(Operation.Sync) ||
@ -251,6 +259,7 @@ export class ActionButtonCommand {
...this.state,
HEAD: this.repository.HEAD,
isMergeInProgress: this.repository.mergeGroup.resourceStates.length !== 0,
isRebaseInProgress: !!this.repository.rebaseCommit,
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit()
};
}

View file

@ -432,11 +432,11 @@ export class CommandCenter {
]);
// ours (current branch and commit)
ours.detail = head.refNames.map(s => s.replace(/^HEAD ->/, '')).join(', ');
ours.description = head.hash.substring(0, 7);
ours.description = '$(git-commit) ' + head.hash.substring(0, 7);
// theirs
theirs.detail = rebaseOrMergeHead.refNames.join(', ');
theirs.description = rebaseOrMergeHead.hash.substring(0, 7);
theirs.description = '$(git-commit) ' + rebaseOrMergeHead.hash.substring(0, 7);
} catch (error) {
// not so bad, can continue with just uris
@ -469,7 +469,7 @@ export class CommandCenter {
/* __GDPR__
"clone" : {
"owner": "lszomoru",
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
@ -495,7 +495,7 @@ export class CommandCenter {
/* __GDPR__
"clone" : {
"owner": "lszomoru",
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
@ -554,8 +554,8 @@ export class CommandCenter {
/* __GDPR__
"clone" : {
"owner": "lszomoru",
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" },
"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Indicates whether the folder is opened following the clone operation" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: action === PostCloneAction.Open || action === PostCloneAction.OpenNewWindow ? 1 : 0 });
@ -574,7 +574,7 @@ export class CommandCenter {
/* __GDPR__
"clone" : {
"owner": "lszomoru",
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
@ -584,7 +584,7 @@ export class CommandCenter {
/* __GDPR__
"clone" : {
"owner": "lszomoru",
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
@ -1524,7 +1524,7 @@ export class CommandCenter {
}
if (opts.all === undefined) {
opts = { all: noStagedChanges };
opts = { ...opts, all: noStagedChanges };
} else if (!opts.all && noStagedChanges && !opts.empty) {
opts = { ...opts, all: true };
}
@ -3066,7 +3066,7 @@ export class CommandCenter {
/* __GDPR__
"git.command" : {
"owner": "lszomoru",
"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The command id of the command being executed" }
}
*/
this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

View file

@ -1475,7 +1475,7 @@ export class Repository {
const args = ['rebase', '--continue'];
try {
await this.exec(args);
await this.exec(args, { env: { GIT_EDITOR: 'true' } });
} catch (commitErr) {
await this.handleCommitError(commitErr);
}

View file

@ -617,7 +617,7 @@ class ResourceCommandResolver {
if (!resource.leftUri) {
const bothModified = resource.type === Status.BOTH_MODIFIED;
if (resource.rightUri && bothModified && workspace.getConfiguration('git').get<boolean>('mergeEditor', false)) {
if (resource.rightUri && workspace.getConfiguration('git').get<boolean>('mergeEditor', false) && (bothModified || resource.type === Status.BOTH_ADDED)) {
return {
command: '_git.openMergeEditor',
title: localize('open.merge', "Open Merge"),
@ -1894,9 +1894,9 @@ export class Repository implements Disposable {
/* __GDPR__
"statusLimit" : {
"owner": "lszomoru",
"ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"limit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
"ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting indicating whether submodules are ignored" },
"limit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Setting indicating the limit of status entries" },
"statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of status entries" }
}
*/
this.telemetryReporter.sendTelemetryEvent('statusLimit', { ignoreSubmodules: String(ignoreSubmodules) }, { limit, statusLength });
@ -2028,6 +2028,9 @@ export class Repository implements Disposable {
// set count badge
this.setCountBadge();
// set mergeChanges context
commands.executeCommand('setContext', 'git.mergeChanges', merge.map(item => item.resourceUri.toString()));
this._onDidChangeStatus.fire();
this._sourceControl.commitTemplate = await this.getInputTemplate();

View file

@ -36,6 +36,12 @@
"scopeName": "text.html.handlebars",
"path": "./syntaxes/Handlebars.tmLanguage.json"
}
],
"htmlLanguageParticipants": [
{
"languageId": "handlebars",
"autoInsert": true
}
]
},
"repository": {

View file

@ -5,8 +5,9 @@
import { window, workspace, Disposable, TextDocument, Position, SnippetString, TextDocumentChangeEvent, TextDocumentChangeReason, TextDocumentContentChangeEvent } from 'vscode';
import { Runtime } from './htmlClient';
import { LanguageParticipants } from './languageParticipants';
export function activateAutoInsertion(provider: (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position) => Thenable<string>, supportedLanguages: { [id: string]: boolean }, runtime: Runtime): Disposable {
export function activateAutoInsertion(provider: (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position) => Thenable<string>, languageParticipants: LanguageParticipants, runtime: Runtime): Disposable {
const disposables: Disposable[] = [];
workspace.onDidChangeTextDocument(onDidChangeTextDocument, null, disposables);
@ -33,7 +34,7 @@ export function activateAutoInsertion(provider: (kind: 'autoQuote' | 'autoClose'
return;
}
const document = editor.document;
if (!supportedLanguages[document.languageId]) {
if (!languageParticipants.useAutoInsert(document.languageId)) {
return;
}
const configurations = workspace.getConfiguration(undefined, document.uri);

View file

@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, ExtensionContext, Uri } from 'vscode';
import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient';
import { startClient, LanguageClientConstructor } from '../htmlClient';
import { LanguageClientOptions } from 'vscode-languageclient';
import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient';
import { LanguageClient } from 'vscode-languageclient/browser';
declare const Worker: {
@ -15,7 +15,7 @@ declare const TextDecoder: {
new(encoding?: string): { decode(buffer: ArrayBuffer): string };
};
let client: BaseLanguageClient | undefined;
let client: AsyncDisposable | undefined;
// this method is called when vs code is activated
export async function activate(context: ExtensionContext) {
@ -42,7 +42,7 @@ export async function activate(context: ExtensionContext) {
export async function deactivate(): Promise<void> {
if (client) {
await client.stop();
await client.dispose();
client = undefined;
}
}

View file

@ -125,7 +125,7 @@ function collectInWorkspaces(workspaceUris: Set<string>): Set<string> {
}
function collectInExtensions(localExtensionUris: Set<string>, externalUris: Set<string>): void {
for (const extension of extensions.all) {
for (const extension of extensions.allAcrossExtensionHosts) {
const customData = extension.packageJSON?.contributes?.html?.customData;
if (Array.isArray(customData)) {
for (const uriOrPath of customData) {

View file

@ -9,7 +9,7 @@ const localize = nls.loadMessageBundle();
import {
languages, ExtensionContext, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, extensions,
Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList, SemanticTokensLegend,
DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands
DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands, OutputChannel
} from 'vscode';
import {
LanguageClientOptions, RequestType, DocumentRangeFormattingParams,
@ -18,6 +18,7 @@ import {
import { FileSystemProvider, serveFileSystemRequests } from './requests';
import { getCustomDataSource } from './customData';
import { activateAutoInsertion } from './autoInsertion';
import { getLanguageParticipants, LanguageParticipants } from './languageParticipants';
namespace CustomDataChangedNotification {
export const type: NotificationType<string[]> = new NotificationType('html/customDataChanged');
@ -74,6 +75,8 @@ export interface TelemetryReporter {
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
export const languageServerDescription = localize('htmlserver.name', 'HTML Language Server');
export interface Runtime {
TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string } };
fileFs?: FileSystemProvider;
@ -83,11 +86,69 @@ export interface Runtime {
};
}
export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<BaseLanguageClient> {
export interface AsyncDisposable {
dispose(): Promise<void>;
}
const toDispose = context.subscriptions;
export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {
const documentSelector = ['html', 'handlebars'];
const outputChannel = window.createOutputChannel(languageServerDescription);
const languageParticipants = getLanguageParticipants();
context.subscriptions.push(languageParticipants);
let client: Disposable | undefined = await startClientWithParticipants(languageParticipants, newLanguageClient, outputChannel, runtime);
const promptForLinkedEditingKey = 'html.promptForLinkedEditing';
if (extensions.getExtension('formulahendry.auto-rename-tag') !== undefined && (context.globalState.get(promptForLinkedEditingKey) !== false)) {
const config = workspace.getConfiguration('editor', { languageId: 'html' });
if (!config.get('linkedEditing') && !config.get('renameOnType')) {
const activeEditorListener = window.onDidChangeActiveTextEditor(async e => {
if (e && languageParticipants.hasLanguage(e.document.languageId)) {
context.globalState.update(promptForLinkedEditingKey, false);
activeEditorListener.dispose();
const configure = localize('configureButton', 'Configure');
const res = await window.showInformationMessage(localize('linkedEditingQuestion', 'VS Code now has built-in support for auto-renaming tags. Do you want to enable it?'), configure);
if (res === configure) {
commands.executeCommand('workbench.action.openSettings', SettingIds.linkedEditing);
}
}
});
context.subscriptions.push(activeEditorListener);
}
}
let restartTrigger: Disposable | undefined;
languageParticipants.onDidChange(() => {
if (restartTrigger) {
restartTrigger.dispose();
}
restartTrigger = runtime.timer.setTimeout(async () => {
if (client) {
outputChannel.appendLine('Extensions have changed, restarting HTML server...');
outputChannel.appendLine('');
const oldClient = client;
client = undefined;
await oldClient.dispose();
client = await startClientWithParticipants(languageParticipants, newLanguageClient, outputChannel, runtime);
}
}, 2000);
});
return {
dispose: async () => {
restartTrigger?.dispose();
await client?.dispose();
outputChannel.dispose();
}
};
}
async function startClientWithParticipants(languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, outputChannel: OutputChannel, runtime: Runtime): Promise<AsyncDisposable> {
const toDispose: Disposable[] = [];
const documentSelector = languageParticipants.documentSelector;
const embeddedLanguages = { css: true, javascript: true };
let rangeFormatting: Disposable | undefined = undefined;
@ -129,22 +190,23 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
}
}
};
clientOptions.outputChannel = outputChannel;
// Create the language client and start the client.
const client = newLanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), clientOptions);
const client = newLanguageClient('html', languageServerDescription, clientOptions);
client.registerProposedFeatures();
await client.start();
toDispose.push(serveFileSystemRequests(client, runtime));
const customDataSource = getCustomDataSource(runtime, context.subscriptions);
const customDataSource = getCustomDataSource(runtime, toDispose);
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
customDataSource.onDidChange(() => {
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
});
client.onRequest(CustomDataContent.type, customDataSource.getContent);
}, undefined, toDispose);
toDispose.push(client.onRequest(CustomDataContent.type, customDataSource.getContent));
const insertRequestor = (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position): Promise<string> => {
@ -155,7 +217,8 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
};
return client.sendRequest(AutoInsertRequest.type, param);
};
const disposable = activateAutoInsertion(insertRequestor, { html: true, handlebars: true }, runtime);
const disposable = activateAutoInsertion(insertRequestor, languageParticipants, runtime);
toDispose.push(disposable);
const disposable2 = client.onTelemetry(e => {
@ -193,7 +256,6 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
}
});
function updateFormatterRegistration() {
const formatEnabled = workspace.getConfiguration().get(SettingIds.formatEnable);
if (!formatEnabled && rangeFormatting) {
@ -278,25 +340,12 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
}
}));
const promptForLinkedEditingKey = 'html.promptForLinkedEditing';
if (extensions.getExtension('formulahendry.auto-rename-tag') !== undefined && (context.globalState.get(promptForLinkedEditingKey) !== false)) {
const config = workspace.getConfiguration('editor', { languageId: 'html' });
if (!config.get('linkedEditing') && !config.get('renameOnType')) {
const activeEditorListener = window.onDidChangeActiveTextEditor(async e => {
if (e && documentSelector.indexOf(e.document.languageId) !== -1) {
context.globalState.update(promptForLinkedEditingKey, false);
activeEditorListener.dispose();
const configure = localize('configureButton', 'Configure');
const res = await window.showInformationMessage(localize('linkedEditingQuestion', 'VS Code now has built-in support for auto-renaming tags. Do you want to enable it?'), configure);
if (res === configure) {
commands.executeCommand('workbench.action.openSettings', SettingIds.linkedEditing);
}
}
});
toDispose.push(activeEditorListener);
return {
dispose: async () => {
await client.stop();
toDispose.forEach(d => d.dispose());
rangeFormatting?.dispose();
}
}
return client;
};
}

View file

@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DocumentSelector } from 'vscode-languageclient';
import { Event, EventEmitter, extensions } from 'vscode';
/**
* HTML language participant contribution.
*/
interface LanguageParticipantContribution {
/**
* The id of the language which participates with the HTML language server.
*/
languageId: string;
/**
* true if the language activates the auto insertion and false otherwise.
*/
autoInsert?: boolean;
}
export interface LanguageParticipants {
readonly onDidChange: Event<void>;
readonly documentSelector: DocumentSelector;
hasLanguage(languageId: string): boolean;
useAutoInsert(languageId: string): boolean;
dispose(): void;
}
export function getLanguageParticipants(): LanguageParticipants {
const onDidChangeEmmiter = new EventEmitter<void>();
let languages = new Set<string>();
let autoInsert = new Set<string>();
function update() {
const oldLanguages = languages, oldAutoInsert = autoInsert;
languages = new Set();
languages.add('html');
autoInsert = new Set();
autoInsert.add('html');
for (const extension of extensions.allAcrossExtensionHosts) {
const htmlLanguageParticipants = extension.packageJSON?.contributes?.htmlLanguageParticipants as LanguageParticipantContribution[];
if (Array.isArray(htmlLanguageParticipants)) {
for (const htmlLanguageParticipant of htmlLanguageParticipants) {
const languageId = htmlLanguageParticipant.languageId;
if (typeof languageId === 'string') {
languages.add(languageId);
if (htmlLanguageParticipant.autoInsert !== false) {
autoInsert.add(languageId);
}
}
}
}
}
return !isEqualSet(languages, oldLanguages) || !isEqualSet(oldLanguages, oldAutoInsert);
}
update();
const changeListener = extensions.onDidChange(_ => {
if (update()) {
onDidChangeEmmiter.fire();
}
});
return {
onDidChange: onDidChangeEmmiter.event,
get documentSelector() { return Array.from(languages); },
hasLanguage(languageId: string) { return languages.has(languageId); },
useAutoInsert(languageId: string) { return autoInsert.has(languageId); },
dispose: () => changeListener.dispose()
};
}
function isEqualSet<T>(s1: Set<T>, s2: Set<T>) {
if (s1.size !== s2.size) {
return false;
}
for (const e of s1) {
if (!s2.has(e)) {
return false;
}
}
return true;
}

View file

@ -5,15 +5,15 @@
import { getNodeFileFS } from './nodeFs';
import { Disposable, ExtensionContext } from 'vscode';
import { startClient, LanguageClientConstructor } from '../htmlClient';
import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node';
import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient';
import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node';
import { TextDecoder } from 'util';
import * as fs from 'fs';
import TelemetryReporter from '@vscode/extension-telemetry';
let telemetry: TelemetryReporter | undefined;
let client: BaseLanguageClient | undefined;
let client: AsyncDisposable | undefined;
// this method is called when vs code is activated
export async function activate(context: ExtensionContext) {
@ -50,7 +50,7 @@ export async function activate(context: ExtensionContext) {
export async function deactivate(): Promise<void> {
if (client) {
await client.stop();
await client.dispose();
client = undefined;
}
}

View file

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

View file

@ -14,6 +14,9 @@
"onLanguage:html",
"onLanguage:handlebars"
],
"enabledApiProposals": [
"extensionsAny"
],
"main": "./client/out/node/htmlClientMain",
"browser": "./client/dist/browser/htmlClientMain",
"capabilities": {

View file

@ -13,6 +13,23 @@
"type": "string",
"description": "Relative path to a HTML custom data file"
}
},
"htmlLanguageParticipants": {
"type": "array",
"description": "A list of languages that participate with the HTML language server.",
"items": {
"type": "object",
"properties": {
"languageId": {
"type": "string",
"description": "The id of the language that participates with HTML language server."
},
"autoInsert": {
"type": "boolean",
"description": "Whether the language participates with HTML auto insertions. If not specified, defaults to <code>true</code>."
}
}
}
}
}
}

View file

@ -10,7 +10,7 @@
"main": "./out/node/htmlServerMain",
"dependencies": {
"vscode-css-languageservice": "^6.0.1",
"vscode-html-languageservice": "^5.0.0",
"vscode-html-languageservice": "^5.0.1",
"vscode-languageserver": "^8.0.2-next.4",
"vscode-languageserver-textdocument": "^1.0.4",
"vscode-nls": "^5.0.1",

View file

@ -24,6 +24,10 @@ export function getDocumentContext(documentUri: string, workspaceFolders: Worksp
return {
resolveReference: (ref: string, base = documentUri) => {
if (ref.match(/^\w[\w\d+.-]*:/)) {
// starts with a schema
return ref;
}
if (ref[0] === '/') { // resolve absolute path against the current workspace folder
const folderUri = getRootFolder();
if (folderUri) {

View file

@ -22,10 +22,10 @@ vscode-css-languageservice@^6.0.1:
vscode-nls "^5.0.1"
vscode-uri "^3.0.3"
vscode-html-languageservice@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.0.0.tgz#c68613f836d7fcff125183d78e6f1f0ff326fa55"
integrity sha512-KJG13z54aLszskp3ETf8b1EKDypr2Sf5RUsfR6OXmKqEl2ZUfyIxsWz4gbJWjPzoJZx/bGH0ZXVwxJ1rg8OKRQ==
vscode-html-languageservice@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.0.1.tgz#bdf7847d27a453a9e98ae2836ead7594784c5c1c"
integrity sha512-OYsyn5HGAhxs0OIG+M0jc34WnftLtD67Wg7+TfrYwvf0waOkkr13zUqtdrVm2JPNQ6fJx+qnuM+vTbq7o1dCdQ==
dependencies:
vscode-languageserver-textdocument "^1.0.4"
vscode-languageserver-types "^3.17.1"

View file

@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
const path = require('path');
const fse = require('fs-extra');
const esbuild = require('esbuild');
const args = process.argv.slice(2);
const isWatch = args.indexOf('--watch') >= 0;
let outputRoot = __dirname;
const outputRootIndex = args.indexOf('--outputRoot');
if (outputRootIndex >= 0) {
outputRoot = args[outputRootIndex + 1];
}
const srcDir = path.join(__dirname, 'src');
const outDir = path.join(outputRoot, 'out');
async function build() {
await esbuild.build({
entryPoints: [
path.join(srcDir, 'cellAttachmentRenderer.ts'),
],
bundle: true,
minify: false,
sourcemap: false,
format: 'esm',
outdir: outDir,
platform: 'browser',
target: ['es2020'],
});
}
build().catch(() => process.exit(1));
if (isWatch) {
const watcher = require('@parcel/watcher');
watcher.subscribe(srcDir, () => {
return build();
});
}

View file

@ -51,6 +51,16 @@
"priority": "default"
}
],
"notebookRenderer": [
{
"id": "vscode.markdown-it-cell-attachment-renderer",
"displayName": "Markdown it ipynb Cell Attachment renderer",
"entrypoint": {
"extends": "vscode.markdown-it-renderer",
"path": "./out/cellAttachmentRenderer.js"
}
}
],
"menus": {
"file/newFile": [
{
@ -70,8 +80,9 @@
}
},
"scripts": {
"compile": "npx gulp compile-extension:ipynb",
"watch": "npx gulp watch-extension:ipynb"
"compile": "npx gulp compile-extension:ipynb && npm run build-notebook",
"watch": "npx gulp watch-extension:ipynb",
"build-notebook": "node ./esbuild"
},
"dependencies": {
"@enonic/fnv-plus": "^1.3.0",
@ -80,6 +91,7 @@
},
"devDependencies": {
"@jupyterlab/nbformat": "^3.2.9",
"@types/markdown-it": "12.2.3",
"@types/uuid": "^8.3.1"
},
"repository": {

View file

@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as MarkdownIt from 'markdown-it';
import type * as MarkdownItToken from 'markdown-it/lib/token';
import type { RendererContext } from 'vscode-notebook-renderer';
interface MarkdownItRenderer {
extendMarkdownIt(fn: (md: MarkdownIt) => void): void;
}
export async function activate(ctx: RendererContext<void>) {
const markdownItRenderer = (await ctx.getRenderer('vscode.markdown-it-renderer')) as MarkdownItRenderer | any;
if (!markdownItRenderer) {
throw new Error(`Could not load 'vscode.markdown-it-renderer'`);
}
markdownItRenderer.extendMarkdownIt((md: MarkdownIt) => {
const original = md.renderer.rules.image;
md.renderer.rules.image = (tokens: MarkdownItToken[], idx: number, options, env, self) => {
const token = tokens[idx];
const src = token.attrGet('src');
const attachments: Record<string, Record<string, string>> = env.outputItem.metadata?.custom?.attachments;
if (attachments && src) {
const [attachmentKey, attachmentVal] = Object.entries(attachments[src.replace('attachment:', '')])[0];
const b64Markdown = 'data:' + attachmentKey + ';base64,' + attachmentVal;
token.attrSet('src', b64Markdown);
}
if (original) {
return original(tokens, idx, options, env, self);
} else {
return self.renderToken(tokens, idx, options);
}
};
});
}

View file

@ -8,21 +8,39 @@
integrity sha512-BCN9uNWH8AmiP7BXBJqEinUY9KXalmRzo+L0cB/mQsmFfzODxwQrbvxCHXUNH2iP+qKkWYtB4vyy8N62PViMFw==
"@jupyterlab/nbformat@^3.2.9":
version "3.2.9"
resolved "https://registry.yarnpkg.com/@jupyterlab/nbformat/-/nbformat-3.2.9.tgz#e7d854719612133498af4280d9a8caa0873205b0"
integrity sha512-WSf9OQo8yfFjyodbXRdFoaNwMkaAL5jFZiD6V2f8HqI380ipansWrrV7R9CGzPfgKHpUGZMO1tYKmUwzMhvZ4w==
version "3.4.3"
resolved "https://registry.yarnpkg.com/@jupyterlab/nbformat/-/nbformat-3.4.3.tgz#cbab1bf507677b7f0f309d8353fc83fe5a973c82"
integrity sha512-i/yADrwhhAJJCUOTa+fEBMyJO7fvX9Y73I0B7V6dQhGcrmrEKLC3wk4yOo63+jRntd5+dupbiOtz3w1ncIXwIA==
dependencies:
"@lumino/coreutils" "^1.5.3"
"@lumino/coreutils" "^1.11.0"
"@lumino/coreutils@^1.5.3":
"@lumino/coreutils@^1.11.0":
version "1.12.0"
resolved "https://registry.yarnpkg.com/@lumino/coreutils/-/coreutils-1.12.0.tgz#fbdef760f736eaf2bd396a5c6fc3a68a4b449b15"
integrity sha512-DSglh4ylmLi820CNx9soJmDJCpUgymckdWeGWuN0Ash5g60oQvrQDfosVxEhzmNvtvXv45WZEqSBzDP6E5SEmQ==
"@types/linkify-it@*":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9"
integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==
"@types/markdown-it@12.2.3":
version "12.2.3"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51"
integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==
dependencies:
"@types/linkify-it" "*"
"@types/mdurl" "*"
"@types/mdurl@*":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
"@types/uuid@^8.3.1":
version "8.3.1"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
detect-indent@^6.0.0:
version "6.1.0"

View file

@ -398,16 +398,27 @@
"description": "%configuration.markdown.suggest.paths.enabled.description%",
"scope": "resource"
},
"markdown.trace": {
"markdown.trace.extension": {
"type": "string",
"enum": [
"off",
"verbose"
],
"default": "off",
"description": "%markdown.trace.desc%",
"description": "%markdown.trace.extension.desc%",
"scope": "window"
},
"markdown.trace.server": {
"type": "string",
"scope": "window",
"enum": [
"off",
"messages",
"verbose"
],
"default": "off",
"description": "%markdown.trace.server.desc%"
},
"markdown.editor.drop.enabled": {
"type": "boolean",
"default": true,
@ -478,6 +489,7 @@
"type": "string",
"scope": "resource",
"markdownDescription": "%configuration.markdown.experimental.validate.fileLinks.markdownFragmentLinks.description%",
"default": "ignore",
"enum": [
"ignore",
"warning",
@ -563,7 +575,8 @@
"@types/vscode-notebook-renderer": "^1.60.0",
"@types/vscode-webview": "^1.57.0",
"lodash.throttle": "^4.1.1",
"vscode-languageserver-types": "^3.17.2"
"vscode-languageserver-types": "^3.17.2",
"vscode-markdown-languageservice": "^0.0.0-alpha.10"
},
"repository": {
"type": "git",

View file

@ -17,7 +17,8 @@
"markdown.showSource.title": "Show Source",
"markdown.styles.dec": "A list of URLs or local paths to CSS style sheets to use from the Markdown preview. Relative paths are interpreted relative to the folder open in the Explorer. If there is no open folder, they are interpreted relative to the location of the Markdown file. All '\\' need to be written as '\\\\'.",
"markdown.showPreviewSecuritySelector.title": "Change Preview Security Settings",
"markdown.trace.desc": "Enable debug logging for the Markdown extension.",
"markdown.trace.extension.desc": "Enable debug logging for the Markdown extension.",
"markdown.trace.server.desc": "Traces the communication between VS Code and the Markdown language server.",
"markdown.preview.refresh.title": "Refresh Preview",
"markdown.preview.toggleLock.title": "Toggle Preview Locking",
"markdown.findAllFileReferences": "Find File References",

View file

@ -0,0 +1,12 @@
.vscode/
.github/
out/test/
src/
.eslintrc.js
.gitignore
tsconfig*.json
*.tsbuildinfo
*.map
example.cjs
CODE_OF_CONDUCT.md
SECURITY.md

View file

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

View file

@ -0,0 +1,2 @@
{
}

View file

@ -0,0 +1,120 @@
# Markdown Language Server
> **❗ Import** This is still in development. While the language server is being used by VS Code, it has not yet been tested with other clients.
The Markdown language server powers VS Code's built-in markdown support, providing tools for writing and browsing Markdown files. It runs as a separate executable and implements the [language server protocol](https://microsoft.github.io/language-server-protocol/overview).
This server uses the [Markdown Language Service](https://github.com/microsoft/vscode-markdown-languageservice) to implement almost all of the language features. You can use that library if you need a library for working with Markdown instead of a full language server.
## Server capabilities
- [Completions](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) for Markdown links.
- [Folding](https://microsoft.github.io/language-server-protocol/specification#textDocument_foldingRange) of Markdown regions, block elements, and header sections.
- [Smart selection](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange) for inline elements, block elements, and header sections.
- [Document Symbols](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol) for quick navigation to headers in a document.
- [Workspace Symbols](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) for quick navigation to headers in the workspace
- [Document links](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentLink) for making Markdown links in a document clickable.
- [Find all references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) to headers and links across all Markdown files in the workspace.
- [Rename](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename) of headers and links across all Markdown files in the workspace.
- [Go to definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) from links to headers or link definitions.
- (experimental) [Pull diagnostics (validation)](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics) for links.
## Client requirements
### Initialization options
The client can send the following initialization options to the server:
- `markdownFileExtensions` Array file extensions that should be considered as Markdown. These should not include the leading `.`. For example: `['md', 'mdown', 'markdown']`.
### Settings
Clients may send a `workspace/didChangeConfiguration` notification to notify the server of settings changes.
The server supports the following settings:
- `markdown`
- `suggest`
- `paths`
- `enabled` — Enable/disable path suggestions.
- `experimental`
- `validate`
- `enabled` Enable/disable all validation.
- `referenceLinks`
- `enabled` Enable/disable validation of reference links: `[text][ref]`
- `fragmentLinks`
- `enabled` Enable/disable validation of links to fragments in the current files: `[text](#head)`
- `fileLinks`
- `enabled` Enable/disable validation of links to file in the workspace.
- `markdownFragmentLinks` Enable/disable validation of links to headers in other Markdown files.
- `ignoreLinks` Array of glob patterns for files that should not be validated.
### Custom requests
To support all of the features of the language server, the client needs to implement a few custom request types. The definitions of these request types can be found in [`protocol.ts`](./src/protocol.ts)
#### `markdown/parse`
Get the tokens for a Markdown file. Clients are expected to use [Markdown-it](https://github.com/markdown-it/markdown-it) for this.
We require that clients bring their own version of Markdown-it so that they can customize/extend Markdown-it.
#### `markdown/fs/readFile`
Read the contents of a file in the workspace.
#### `markdown/fs/readDirectory`
Read the contents of a directory in the workspace.
#### `markdown/fs/stat`
Check if a given file/directory exists in the workspace.
#### `markdown/fs/watcher/create`
Create a file watcher. This is needed for diagnostics support.
#### `markdown/fs/watcher/delete`
Delete a previously created file watcher.
#### `markdown/findMarkdownFilesInWorkspace`
Get a list of all markdown files in the workspace.
## Contribute
The source code of the Markdown language server can be found in the [VSCode repository](https://github.com/microsoft/vscode) at [extensions/markdown-language-features/server](https://github.com/microsoft/vscode/tree/master/extensions/markdown-language-features/server).
File issues and pull requests in the [VSCode GitHub Issues](https://github.com/microsoft/vscode/issues). See the document [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) on how to build and run from source.
Most of the functionality of the server is located in libraries:
- [vscode-markdown-languageservice](https://github.com/microsoft/vscode-markdown-languageservice) contains the implementation of all features as a reusable library.
- [vscode-languageserver-node](https://github.com/microsoft/vscode-languageserver-node) contains the implementation of language server for NodeJS.
Help on any of these projects is very welcome.
## Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
## License
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the [MIT](https://github.com/microsoft/vscode/blob/master/LICENSE.txt) License.

View file

@ -1,7 +1,7 @@
{
"name": "vscode-markdown-languageserver",
"description": "Markdown language server",
"version": "1.0.0",
"version": "0.0.0-alpha-1",
"author": "Microsoft Corporation",
"license": "MIT",
"engines": {
@ -13,7 +13,7 @@
"vscode-languageserver": "^8.0.2-next.5`",
"vscode-languageserver-textdocument": "^1.0.5",
"vscode-languageserver-types": "^3.17.1",
"vscode-markdown-languageservice": "^0.0.0-alpha.8",
"vscode-markdown-languageservice": "^0.0.0-alpha.11",
"vscode-uri": "^3.0.3"
},
"devDependencies": {

View file

@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Connection, Emitter } from 'vscode-languageserver';
import { Disposable } from './util/dispose';
export type ValidateEnabled = 'ignore' | 'warning' | 'error';
interface Settings {
readonly markdown: {
readonly suggest: {
readonly paths: {
readonly enabled: boolean;
};
};
readonly experimental: {
readonly validate: {
readonly enabled: true;
readonly referenceLinks: {
readonly enabled: ValidateEnabled;
};
readonly fragmentLinks: {
readonly enabled: ValidateEnabled;
};
readonly fileLinks: {
readonly enabled: ValidateEnabled;
readonly markdownFragmentLinks: ValidateEnabled;
};
readonly ignoreLinks: readonly string[];
};
};
};
}
export class ConfigurationManager extends Disposable {
private readonly _onDidChangeConfiguration = this._register(new Emitter<Settings>());
public readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event;
private _settings?: Settings;
constructor(connection: Connection) {
super();
// The settings have changed. Is send on server activation as well.
this._register(connection.onDidChangeConfiguration((change) => {
this._settings = change.settings;
this._onDidChangeConfiguration.fire(this._settings!);
}));
}
public getSettings(): Settings | undefined {
return this._settings;
}
}

View file

@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Connection, FullDocumentDiagnosticReport, UnchangedDocumentDiagnosticReport } from 'vscode-languageserver';
import * as md from 'vscode-markdown-languageservice';
import { disposeAll } from 'vscode-markdown-languageservice/out/util/dispose';
import { Disposable } from 'vscode-notebook-renderer/events';
import { URI } from 'vscode-uri';
import { ConfigurationManager, ValidateEnabled } from '../configuration';
import { VsCodeClientWorkspace } from '../workspace';
const defaultDiagnosticOptions: md.DiagnosticOptions = {
validateFileLinks: md.DiagnosticLevel.ignore,
validateReferences: md.DiagnosticLevel.ignore,
validateFragmentLinks: md.DiagnosticLevel.ignore,
validateMarkdownFileLinkFragments: md.DiagnosticLevel.ignore,
ignoreLinks: [],
};
function convertDiagnosticLevel(enabled: ValidateEnabled): md.DiagnosticLevel | undefined {
switch (enabled) {
case 'error': return md.DiagnosticLevel.error;
case 'warning': return md.DiagnosticLevel.warning;
case 'ignore': return md.DiagnosticLevel.ignore;
default: return md.DiagnosticLevel.ignore;
}
}
function getDiagnosticsOptions(config: ConfigurationManager): md.DiagnosticOptions {
const settings = config.getSettings();
if (!settings) {
return defaultDiagnosticOptions;
}
return {
validateFileLinks: convertDiagnosticLevel(settings.markdown.experimental.validate.fileLinks.enabled),
validateReferences: convertDiagnosticLevel(settings.markdown.experimental.validate.referenceLinks.enabled),
validateFragmentLinks: convertDiagnosticLevel(settings.markdown.experimental.validate.fragmentLinks.enabled),
validateMarkdownFileLinkFragments: convertDiagnosticLevel(settings.markdown.experimental.validate.fileLinks.markdownFragmentLinks),
ignoreLinks: settings.markdown.experimental.validate.ignoreLinks,
};
}
export function registerValidateSupport(
connection: Connection,
workspace: VsCodeClientWorkspace,
ls: md.IMdLanguageService,
config: ConfigurationManager,
): Disposable {
let diagnosticOptions: md.DiagnosticOptions = defaultDiagnosticOptions;
function updateDiagnosticsSetting(): void {
diagnosticOptions = getDiagnosticsOptions(config);
}
const subs: Disposable[] = [];
const manager = ls.createPullDiagnosticsManager();
subs.push(manager);
subs.push(manager.onLinkedToFileChanged(() => {
// TODO: We only need to refresh certain files
connection.languages.diagnostics.refresh();
}));
connection.languages.diagnostics.on(async (params, token): Promise<FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport> => {
if (!config.getSettings()?.markdown.experimental.validate.enabled) {
return { kind: 'full', items: [] };
}
const document = await workspace.openMarkdownDocument(URI.parse(params.textDocument.uri));
if (!document) {
return { kind: 'full', items: [] };
}
const diagnostics = await manager.computeDiagnostics(document, diagnosticOptions, token);
return {
kind: 'full',
items: diagnostics,
};
});
updateDiagnosticsSetting();
subs.push(config.onDidChangeConfiguration(() => {
updateDiagnosticsSetting();
connection.languages.diagnostics.refresh();
}));
return {
dispose: () => {
disposeAll(subs);
}
};
}

View file

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ILogger } from 'vscode-markdown-languageservice';
import { ILogger, LogLevel } from 'vscode-markdown-languageservice';
export class LogFunctionLogger implements ILogger {
@ -31,8 +31,9 @@ export class LogFunctionLogger implements ILogger {
private readonly _logFn: typeof console.log
) { }
public verbose(title: string, message: string, data?: any): void {
this.appendLine(`[Verbose ${LogFunctionLogger.now()}] ${title}: ${message}`);
public log(level: LogLevel, title: string, message: string, data?: any): void {
this.appendLine(`[${level} ${LogFunctionLogger.now()}] ${title}: ${message}`);
if (data) {
this.appendLine(LogFunctionLogger.data2String(data));
}

View file

@ -4,15 +4,24 @@
*--------------------------------------------------------------------------------------------*/
import { RequestType } from 'vscode-languageserver';
import * as md from 'vscode-markdown-languageservice';
import * as lsp from 'vscode-languageserver-types';
import type * as lsp from 'vscode-languageserver-types';
import type * as md from 'vscode-markdown-languageservice';
// From server
export const parseRequestType: RequestType<{ uri: string }, md.Token[], any> = new RequestType('markdown/parse');
export const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile');
export const statFileRequestType: RequestType<{ uri: string }, md.FileStat | undefined, any> = new RequestType('markdown/statFile');
export const readDirectoryRequestType: RequestType<{ uri: string }, [string, md.FileStat][], any> = new RequestType('markdown/readDirectory');
export const findFilesRequestTypes: RequestType<{}, string[], any> = new RequestType('markdown/findFiles');
//#region From server
export const parse = new RequestType<{ uri: string }, md.Token[], any>('markdown/parse');
// To server
export const fs_readFile = new RequestType<{ uri: string }, number[], any>('markdown/fs/readFile');
export const fs_readDirectory = new RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any>('markdown/fs/readDirectory');
export const fs_stat = new RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any>('markdown/fs/stat');
export const fs_watcher_create = new RequestType<{ id: number; uri: string; options: md.FileWatcherOptions }, void, any>('markdown/fs/watcher/create');
export const fs_watcher_delete = new RequestType<{ id: number }, void, any>('markdown/fs/watcher/delete');
export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('markdown/findMarkdownFilesInWorkspace');
//#endregion
//#region To server
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');
export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
//#endregion

View file

@ -7,8 +7,11 @@ import { CancellationToken, Connection, InitializeParams, InitializeResult, Note
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as lsp from 'vscode-languageserver-types';
import * as md from 'vscode-markdown-languageservice';
import { IDisposable } from 'vscode-markdown-languageservice/out/util/dispose';
import { URI } from 'vscode-uri';
import { getLsConfiguration } from './config';
import { ConfigurationManager } from './configuration';
import { registerValidateSupport } from './languageFeatures/diagnostics';
import { LogFunctionLogger } from './logging';
import * as protocol from './protocol';
import { VsCodeClientWorkspace } from './workspace';
@ -17,12 +20,17 @@ export async function startServer(connection: Connection) {
const documents = new TextDocuments(TextDocument);
const notebooks = new NotebookDocuments(documents);
const configurationManager = new ConfigurationManager(connection);
let provider: md.IMdLanguageService | undefined;
let workspace: VsCodeClientWorkspace | undefined;
connection.onInitialize((params: InitializeParams): InitializeResult => {
const parser = new class implements md.IMdParser {
slugifier = md.githubSlugifier;
async tokenize(document: md.ITextDocument): Promise<md.Token[]> {
return await connection.sendRequest(protocol.parseRequestType, { uri: document.uri.toString() });
return await connection.sendRequest(protocol.parse, { uri: document.uri.toString() });
}
};
@ -30,8 +38,8 @@ export async function startServer(connection: Connection) {
markdownFileExtensions: params.initializationOptions.markdownFileExtensions,
});
const workspace = new VsCodeClientWorkspace(connection, config, documents, notebooks);
const logger = new LogFunctionLogger(connection.console.log.bind(connection.console));
workspace = new VsCodeClientWorkspace(connection, config, documents, notebooks, logger);
provider = md.createLanguageService({
workspace,
parser,
@ -39,9 +47,18 @@ export async function startServer(connection: Connection) {
markdownFileExtensions: config.markdownFileExtensions,
});
registerCompletionsSupport(connection, documents, provider, configurationManager);
registerValidateSupport(connection, workspace, provider, configurationManager);
workspace.workspaceFolders = (params.workspaceFolders ?? []).map(x => URI.parse(x.uri));
return {
capabilities: {
diagnosticProvider: {
documentSelector: null,
identifier: 'markdown',
interFileDependencies: true,
workspaceDiagnostics: false,
},
completionProvider: { triggerCharacters: ['.', '/', '#'] },
definitionProvider: true,
documentLinkProvider: { resolveProvider: true },
@ -61,8 +78,6 @@ export async function startServer(connection: Connection) {
});
let provider: md.IMdLanguageService | undefined;
connection.onDocumentLinks(async (params, token): Promise<lsp.DocumentLink[]> => {
try {
const document = documents.get(params.textDocument.uri);
@ -129,18 +144,6 @@ export async function startServer(connection: Connection) {
return [];
});
connection.onCompletion(async (params, token): Promise<lsp.CompletionItem[]> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.getCompletionItems(document, params.position, params.context!, token);
}
} catch (e) {
console.error(e.stack);
}
return [];
});
connection.onReferences(async (params, token): Promise<lsp.Location[]> => {
try {
const document = documents.get(params.textDocument.uri);
@ -204,3 +207,46 @@ export async function startServer(connection: Connection) {
notebooks.listen(connection);
connection.listen();
}
function registerCompletionsSupport(
connection: Connection,
documents: TextDocuments<TextDocument>,
ls: md.IMdLanguageService,
config: ConfigurationManager,
): IDisposable {
// let registration: Promise<IDisposable> | undefined;
function update() {
// TODO: client still makes the request in this case. Figure our how to properly unregister.
return;
// const settings = config.getSettings();
// if (settings?.markdown.suggest.paths.enabled) {
// if (!registration) {
// registration = connection.client.register(CompletionRequest.type);
// }
// } else {
// registration?.then(x => x.dispose());
// registration = undefined;
// }
}
connection.onCompletion(async (params, token): Promise<lsp.CompletionItem[]> => {
try {
const settings = config.getSettings();
if (!settings?.markdown.suggest.paths.enabled) {
return [];
}
const document = documents.get(params.textDocument.uri);
if (document) {
return await ls.getCompletionItems(document, params.position, params.context!, token);
}
} catch (e) {
console.error(e.stack);
}
return [];
});
update();
return config.onDidChangeConfiguration(() => update());
}

View file

@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class MultiDisposeError extends Error {
constructor(
public readonly errors: any[]
) {
super(`Encountered errors while disposing of store. Errors: [${errors.join(', ')}]`);
}
}
export function disposeAll(disposables: Iterable<IDisposable>) {
const errors: any[] = [];
for (const disposable of disposables) {
try {
disposable.dispose();
} catch (e) {
errors.push(e);
}
}
if (errors.length === 1) {
throw errors[0];
} else if (errors.length > 1) {
throw new MultiDisposeError(errors);
}
}
export interface IDisposable {
dispose(): void;
}
export abstract class Disposable {
private _isDisposed = false;
protected _disposables: IDisposable[] = [];
public dispose(): any {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
disposeAll(this._disposables);
}
protected _register<T extends IDisposable>(value: T): T {
if (this._isDisposed) {
value.dispose();
} else {
this._disposables.push(value);
}
return value;
}
protected get isDisposed() {
return this._isDisposed;
}
}
export class DisposableStore extends Disposable {
private readonly items = new Set<IDisposable>();
public override dispose() {
super.dispose();
disposeAll(this.items);
this.items.clear();
}
public add<T extends IDisposable>(item: T): T {
if (this.isDisposed) {
console.warn('Adding to disposed store. Item will be leaked');
}
this.items.add(item);
return item;
}
}

View file

@ -6,7 +6,7 @@
import { Connection, Emitter, FileChangeType, NotebookDocuments, TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as md from 'vscode-markdown-languageservice';
import { ContainingDocumentContext } from 'vscode-markdown-languageservice/out/workspace';
import { ContainingDocumentContext, FileWatcherOptions, IFileSystemWatcher } from 'vscode-markdown-languageservice/out/workspace';
import { URI } from 'vscode-uri';
import { LsConfiguration } from './config';
import * as protocol from './protocol';
@ -18,7 +18,7 @@ import { Schemes } from './util/schemes';
declare const TextDecoder: any;
export class VsCodeClientWorkspace implements md.IWorkspace {
export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching {
private readonly _onDidCreateMarkdownDocument = new Emitter<md.ITextDocument>();
public readonly onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocument.event;
@ -33,11 +33,21 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
private readonly _utf8Decoder = new TextDecoder('utf-8');
private _watcherPool = 0;
private readonly _watchers = new Map<number, {
readonly resource: URI;
readonly options: FileWatcherOptions;
readonly onDidChange: Emitter<URI>;
readonly onDidCreate: Emitter<URI>;
readonly onDidDelete: Emitter<URI>;
}>();
constructor(
private readonly connection: Connection,
private readonly config: LsConfiguration,
private readonly documents: TextDocuments<TextDocument>,
private readonly notebooks: NotebookDocuments<TextDocument>,
private readonly logger: md.ILogger,
) {
documents.onDidOpen(e => {
this._documentCache.delete(URI.parse(e.document.uri));
@ -59,6 +69,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
connection.onDidChangeWatchedFiles(async ({ changes }) => {
for (const change of changes) {
const resource = URI.parse(change.uri);
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: onDidChangeWatchedFiles', `${change.type}: ${resource}`);
switch (change.type) {
case FileChangeType.Changed: {
this._documentCache.delete(resource);
@ -83,6 +94,21 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
}
}
});
connection.onRequest(protocol.fs_watcher_onChange, params => {
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: fs_watcher_onChange', `${params.kind}: ${params.uri}`);
const watcher = this._watchers.get(params.id);
if (!watcher) {
return;
}
switch (params.kind) {
case 'create': watcher.onDidCreate.fire(URI.parse(params.uri)); return;
case 'change': watcher.onDidChange.fire(URI.parse(params.uri)); return;
case 'delete': watcher.onDidDelete.fire(URI.parse(params.uri)); return;
}
});
}
public listen() {
@ -108,7 +134,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
const limiter = new Limiter<md.ITextDocument | undefined>(maxConcurrent);
// Add files on disk
const resources = await this.connection.sendRequest(protocol.findFilesRequestTypes, {});
const resources = await this.connection.sendRequest(protocol.findMarkdownFilesInWorkspace, {});
const onDiskResults = await Promise.all(resources.map(strResource => {
return limiter.queue(async () => {
const resource = URI.parse(strResource);
@ -148,13 +174,13 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
}
try {
const response = await this.connection.sendRequest(protocol.readFileRequestType, { uri: resource.toString() });
const response = await this.connection.sendRequest(protocol.fs_readFile, { uri: resource.toString() });
// TODO: LSP doesn't seem to handle Array buffers well
const bytes = new Uint8Array(response);
// We assume that markdown is in UTF-8
const text = this._utf8Decoder.decode(bytes);
const doc = new md.InMemoryDocument(resource, text, 0);
const doc = TextDocument.create(resource.toString(), 'markdown', 0, text);
this._documentCache.set(resource, doc);
return doc;
} catch (e) {
@ -163,14 +189,16 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
}
async stat(resource: URI): Promise<md.FileStat | undefined> {
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: stat', `${resource}`);
if (this._documentCache.has(resource) || this.documents.get(resource.toString())) {
return { isDirectory: false };
}
return this.connection.sendRequest(protocol.statFileRequestType, { uri: resource.toString() });
return this.connection.sendRequest(protocol.fs_stat, { uri: resource.toString() });
}
async readDirectory(resource: URI): Promise<[string, md.FileStat][]> {
return this.connection.sendRequest(protocol.readDirectoryRequestType, { uri: resource.toString() });
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: readDir', `${resource}`);
return this.connection.sendRequest(protocol.fs_readDirectory, { uri: resource.toString() });
}
getContainingDocument(resource: URI): ContainingDocumentContext | undefined {
@ -186,6 +214,37 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
return undefined;
}
watchFile(resource: URI, options: FileWatcherOptions): IFileSystemWatcher {
const id = this._watcherPool++;
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: watchFile', `(${id}) ${resource}`);
const entry = {
resource,
options,
onDidCreate: new Emitter<URI>(),
onDidChange: new Emitter<URI>(),
onDidDelete: new Emitter<URI>(),
};
this._watchers.set(id, entry);
this.connection.sendRequest(protocol.fs_watcher_create, {
id,
uri: resource.toString(),
options,
});
return {
onDidCreate: entry.onDidCreate.event,
onDidChange: entry.onDidChange.event,
onDidDelete: entry.onDidDelete.event,
dispose: () => {
this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: disposeWatcher', `(${id}) ${resource}`);
this.connection.sendRequest(protocol.fs_watcher_delete, { id });
this._watchers.delete(id);
}
};
}
private isRelevantMarkdownDocument(doc: TextDocument) {
return isMarkdownFile(doc) && URI.parse(doc.uri).scheme !== 'vscode-bulkeditpreview';
}

View file

@ -7,6 +7,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.43.tgz#555e5a743f76b6b897d47f945305b618525ddbe6"
integrity sha512-GqWykok+3uocgfAJM8imbozrqLnPyTrpFlrryURQlw1EesPUCx5XxTiucWDSFF9/NUEXDuD4bnvHm8xfVGWTpQ==
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
vscode-jsonrpc@8.0.2-next.1:
version "8.0.2-next.1"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2-next.1.tgz#6bdc39fd194782032e34047eeefce562941259c6"
@ -42,15 +47,22 @@ vscode-languageserver@^8.0.2-next.5`:
dependencies:
vscode-languageserver-protocol "3.17.2-next.6"
vscode-markdown-languageservice@^0.0.0-alpha.8:
version "0.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.0.0-alpha.8.tgz#05d4f86cf0514fd71479847eef742fcc8cdbe87f"
integrity sha512-si8weZsY4LtyonyZwxpFYk8WucRFiKJisErNTt1HDjUCglSDIZqsMNuMIcz3t0nVNfG0LrpdMFVLGhmET5D71Q==
vscode-markdown-languageservice@^0.0.0-alpha.11:
version "0.0.0-alpha.11"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.0.0-alpha.11.tgz#9dcdfc23f9dcaeaca3b2a2c76fc504619eaeb284"
integrity sha512-syFamf99xx+Q9DkA66+8fbSz2A2LJkyOV+nSziGgAzdDPv4jkb7eWF6l+nUteHTGbRLQ+q0tfkj0A7OovueCSQ==
dependencies:
picomatch "^2.3.1"
vscode-languageserver-textdocument "^1.0.5"
vscode-languageserver-types "^3.17.1"
vscode-nls "^5.0.1"
vscode-uri "^3.0.3"
vscode-nls@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.1.tgz#ba23fc4d4420d25e7f886c8e83cbdcec47aa48b2"
integrity sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A==
vscode-uri@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84"

View file

@ -3,22 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Token = require('markdown-it/lib/token');
import * as vscode from 'vscode';
import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType, RequestType } from 'vscode-languageclient';
import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { IMdParser } from './markdownEngine';
import { markdownFileExtensions } from './util/file';
import * as proto from './protocol';
import { looksLikeMarkdownPath, markdownFileExtensions } from './util/file';
import { IMdWorkspace } from './workspace';
const localize = nls.loadMessageBundle();
const parseRequestType: RequestType<{ uri: string }, Token[], any> = new RequestType('markdown/parse');
const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile');
const statFileRequestType: RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any> = new RequestType('markdown/statFile');
const readDirectoryRequestType: RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any> = new RequestType('markdown/readDirectory');
const findFilesRequestTypes: RequestType<{}, string[], any> = new RequestType('markdown/findFiles');
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
@ -34,7 +28,16 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
},
initializationOptions: {
markdownFileExtensions,
}
},
diagnosticPullOptions: {
onChange: true,
onSave: true,
onTabs: true,
match(_documentSelector, resource) {
return looksLikeMarkdownPath(resource);
},
},
};
const client = factory('markdown', localize('markdownServer.name', 'Markdown Language Server'), clientOptions);
@ -54,7 +57,7 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
});
}
client.onRequest(parseRequestType, async (e) => {
client.onRequest(proto.parse, async (e) => {
const uri = vscode.Uri.parse(e.uri);
const doc = await workspace.getOrLoadMarkdownDocument(uri);
if (doc) {
@ -64,12 +67,12 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
}
});
client.onRequest(readFileRequestType, async (e): Promise<number[]> => {
client.onRequest(proto.fs_readFile, async (e): Promise<number[]> => {
const uri = vscode.Uri.parse(e.uri);
return Array.from(await vscode.workspace.fs.readFile(uri));
});
client.onRequest(statFileRequestType, async (e): Promise<{ isDirectory: boolean } | undefined> => {
client.onRequest(proto.fs_stat, async (e): Promise<{ isDirectory: boolean } | undefined> => {
const uri = vscode.Uri.parse(e.uri);
try {
const stat = await vscode.workspace.fs.stat(uri);
@ -79,16 +82,32 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
}
});
client.onRequest(readDirectoryRequestType, async (e): Promise<[string, { isDirectory: boolean }][]> => {
client.onRequest(proto.fs_readDirectory, async (e): Promise<[string, { isDirectory: boolean }][]> => {
const uri = vscode.Uri.parse(e.uri);
const result = await vscode.workspace.fs.readDirectory(uri);
return result.map(([name, type]) => [name, { isDirectory: type === vscode.FileType.Directory }]);
});
client.onRequest(findFilesRequestTypes, async (): Promise<string[]> => {
client.onRequest(proto.findMarkdownFilesInWorkspace, async (): Promise<string[]> => {
return (await vscode.workspace.findFiles(mdFileGlob, '**/node_modules/**')).map(x => x.toString());
});
const watchers = new Map<number, vscode.FileSystemWatcher>();
client.onRequest(proto.fs_watcher_create, async (params): Promise<void> => {
const id = params.id;
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.parse(params.uri), '*'), params.options.ignoreCreate, params.options.ignoreChange, params.options.ignoreDelete);
watchers.set(id, watcher);
watcher.onDidCreate(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'create' }); });
watcher.onDidChange(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'change' }); });
watcher.onDidDelete(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'delete' }); });
});
client.onRequest(proto.fs_watcher_delete, async (params): Promise<void> => {
watchers.get(params.id)?.dispose();
watchers.delete(params.id);
});
await client.start();
return client;

View file

@ -26,6 +26,9 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(workspace);
const client = await startServer(context, workspace, engine);
context.subscriptions.push({
dispose: () => client.stop()
});
activateShared(context, client, workspace, engine, logger, contributions);
}

View file

@ -9,12 +9,10 @@ import { CommandManager } from './commandManager';
import * as commands from './commands/index';
import { registerPasteSupport } from './languageFeatures/copyPaste';
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
import { MdLinkProvider } from './languageFeatures/documentLinks';
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
import { MdReferencesProvider } from './languageFeatures/references';
import { ILogger } from './logging';
import { IMdParser, MarkdownItEngine, MdParsingProvider } from './markdownEngine';
import { MarkdownItEngine, MdParsingProvider } from './markdownEngine';
import { MarkdownContributionProvider } from './markdownExtensions';
import { MdDocumentRenderer } from './preview/documentRenderer';
import { MarkdownPreviewManager } from './preview/previewManager';
@ -45,7 +43,7 @@ export function activateShared(
const previewManager = new MarkdownPreviewManager(contentProvider, workspace, logger, contributions, tocProvider);
context.subscriptions.push(previewManager);
context.subscriptions.push(registerMarkdownLanguageFeatures(client, parser, workspace, commandManager, tocProvider, logger));
context.subscriptions.push(registerMarkdownLanguageFeatures(client, commandManager));
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine, tocProvider));
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
@ -55,23 +53,12 @@ export function activateShared(
function registerMarkdownLanguageFeatures(
client: BaseLanguageClient,
parser: IMdParser,
workspace: IMdWorkspace,
commandManager: CommandManager,
tocProvider: MdTableOfContentsProvider,
logger: ILogger,
): vscode.Disposable {
const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' };
const linkProvider = new MdLinkProvider(parser, workspace, logger);
const referencesProvider = new MdReferencesProvider(parser, workspace, tocProvider, logger);
return vscode.Disposable.from(
linkProvider,
referencesProvider,
// Language features
registerDiagnosticSupport(selector, workspace, linkProvider, commandManager, referencesProvider, tocProvider, logger),
registerDiagnosticSupport(selector, commandManager),
registerDropIntoEditorSupport(selector),
registerFindFileReferenceSupport(commandManager, client),
registerPasteSupport(selector),

View file

@ -26,6 +26,9 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(workspace);
const client = await startServer(context, workspace, engine);
context.subscriptions.push({
dispose: () => client.stop()
});
activateShared(context, client, workspace, engine, logger, contributions);
}

View file

@ -3,609 +3,20 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as picomatch from 'picomatch';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { CommandManager } from '../commandManager';
import { ILogger } from '../logging';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { ITextDocument } from '../types/textDocument';
import { Delayer } from '../util/async';
import { noopToken } from '../util/cancellation';
import { Disposable } from '../util/dispose';
import { isMarkdownFile, looksLikeMarkdownPath } from '../util/file';
import { Limiter } from '../util/limiter';
import { ResourceMap } from '../util/resourceMap';
import { MdTableOfContentsWatcher } from '../util/tableOfContentsWatcher';
import { IMdWorkspace } from '../workspace';
import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider, MdLinkSource } from './documentLinks';
import { MdReferencesProvider, tryResolveLinkPath } from './references';
const localize = nls.loadMessageBundle();
export interface DiagnosticConfiguration {
/**
* Fired when the configuration changes.
*/
readonly onDidChange: vscode.Event<void>;
getOptions(resource: vscode.Uri): DiagnosticOptions;
// Copied from markdown language service
export enum DiagnosticCode {
link_noSuchReferences = 'link.no-such-reference',
link_noSuchHeaderInOwnFile = 'link.no-such-header-in-own-file',
link_noSuchFile = 'link.no-such-file',
link_noSuchHeaderInFile = 'link.no-such-header-in-file',
}
export enum DiagnosticLevel {
ignore = 'ignore',
warning = 'warning',
error = 'error',
}
export interface DiagnosticOptions {
readonly enabled: boolean;
readonly validateReferences: DiagnosticLevel | undefined;
readonly validateFragmentLinks: DiagnosticLevel | undefined;
readonly validateFileLinks: DiagnosticLevel | undefined;
readonly validateMarkdownFileLinkFragments: DiagnosticLevel | undefined;
readonly ignoreLinks: readonly string[];
}
function toSeverity(level: DiagnosticLevel | undefined): vscode.DiagnosticSeverity | undefined {
switch (level) {
case DiagnosticLevel.error: return vscode.DiagnosticSeverity.Error;
case DiagnosticLevel.warning: return vscode.DiagnosticSeverity.Warning;
case DiagnosticLevel.ignore: return undefined;
case undefined: return undefined;
}
}
class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConfiguration {
private readonly _onDidChange = this._register(new vscode.EventEmitter<void>());
public readonly onDidChange = this._onDidChange.event;
constructor() {
super();
this._register(vscode.workspace.onDidChangeConfiguration(e => {
if (
e.affectsConfiguration('markdown.experimental.validate.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.referenceLinks.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.fragmentLinks.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.fileLinks.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.fileLinks.markdownFragmentLinks')
|| e.affectsConfiguration('markdown.experimental.validate.ignoreLinks')
) {
this._onDidChange.fire();
}
}));
}
public getOptions(resource: vscode.Uri): DiagnosticOptions {
const config = vscode.workspace.getConfiguration('markdown', resource);
const validateFragmentLinks = config.get<DiagnosticLevel>('experimental.validate.fragmentLinks.enabled');
return {
enabled: config.get<boolean>('experimental.validate.enabled', false),
validateReferences: config.get<DiagnosticLevel>('experimental.validate.referenceLinks.enabled'),
validateFragmentLinks,
validateFileLinks: config.get<DiagnosticLevel>('experimental.validate.fileLinks.enabled'),
validateMarkdownFileLinkFragments: config.get<DiagnosticLevel | undefined>('markdown.experimental.validate.fileLinks.markdownFragmentLinks', validateFragmentLinks),
ignoreLinks: config.get('experimental.validate.ignoreLinks', []),
};
}
}
class InflightDiagnosticRequests {
private readonly inFlightRequests = new ResourceMap<{ readonly cts: vscode.CancellationTokenSource }>();
public async trigger(resource: vscode.Uri, compute: (token: vscode.CancellationToken) => Promise<void>): Promise<void> {
this.cancel(resource);
const cts = new vscode.CancellationTokenSource();
const entry = { cts };
this.inFlightRequests.set(resource, entry);
try {
return await compute(cts.token);
} finally {
if (this.inFlightRequests.get(resource) === entry) {
this.inFlightRequests.delete(resource);
}
cts.dispose();
}
}
public cancel(resource: vscode.Uri) {
const existing = this.inFlightRequests.get(resource);
if (existing) {
existing.cts.cancel();
this.inFlightRequests.delete(resource);
}
}
public dispose() {
this.clear();
}
public clear() {
for (const { cts } of this.inFlightRequests.values()) {
cts.dispose();
}
this.inFlightRequests.clear();
}
}
class LinkWatcher extends Disposable {
private readonly _onDidChangeLinkedToFile = this._register(new vscode.EventEmitter<Iterable<vscode.Uri>>);
/**
* Event fired with a list of document uri when one of the links in the document changes
*/
public readonly onDidChangeLinkedToFile = this._onDidChangeLinkedToFile.event;
private readonly _watchers = new ResourceMap<{
/**
* Watcher for this link path
*/
readonly watcher: vscode.Disposable;
/**
* List of documents that reference the link
*/
readonly documents: ResourceMap</* document resource*/ vscode.Uri>;
}>();
override dispose() {
super.dispose();
for (const entry of this._watchers.values()) {
entry.watcher.dispose();
}
this._watchers.clear();
}
/**
* Set the known links in a markdown document, adding and removing file watchers as needed
*/
updateLinksForDocument(document: vscode.Uri, links: readonly MdLink[]) {
const linkedToResource = new Set<vscode.Uri>(
links
.filter(link => link.href.kind === 'internal')
.map(link => (link.href as InternalHref).path));
// First decrement watcher counter for previous document state
for (const entry of this._watchers.values()) {
entry.documents.delete(document);
}
// Then create/update watchers for new document state
for (const path of linkedToResource) {
let entry = this._watchers.get(path);
if (!entry) {
entry = {
watcher: this.startWatching(path),
documents: new ResourceMap(),
};
this._watchers.set(path, entry);
}
entry.documents.set(document, document);
}
// Finally clean up watchers for links that are no longer are referenced anywhere
for (const [key, value] of this._watchers) {
if (value.documents.size === 0) {
value.watcher.dispose();
this._watchers.delete(key);
}
}
}
deleteDocument(resource: vscode.Uri) {
this.updateLinksForDocument(resource, []);
}
private startWatching(path: vscode.Uri): vscode.Disposable {
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(path, '*'), false, true, false);
const handler = (resource: vscode.Uri) => this.onLinkedResourceChanged(resource);
return vscode.Disposable.from(
watcher,
watcher.onDidDelete(handler),
watcher.onDidCreate(handler),
);
}
private onLinkedResourceChanged(resource: vscode.Uri) {
const entry = this._watchers.get(resource);
if (entry) {
this._onDidChangeLinkedToFile.fire(entry.documents.values());
}
}
}
class LinkDoesNotExistDiagnostic extends vscode.Diagnostic {
public readonly link: string;
constructor(range: vscode.Range, message: string, severity: vscode.DiagnosticSeverity, link: string) {
super(range, message, severity);
this.link = link;
}
}
export abstract class DiagnosticReporter extends Disposable {
private readonly pending = new Set<Promise<any>>();
public clear(): void {
this.pending.clear();
}
public abstract set(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]): void;
public abstract delete(uri: vscode.Uri): void;
public abstract isOpen(uri: vscode.Uri): boolean;
public abstract getOpenDocuments(): ITextDocument[];
public addWorkItem(promise: Promise<any>): Promise<any> {
this.pending.add(promise);
promise.finally(() => this.pending.delete(promise));
return promise;
}
public async waitPendingWork(): Promise<void> {
await Promise.all([...this.pending.values()]);
}
}
export class DiagnosticCollectionReporter extends DiagnosticReporter {
private readonly collection: vscode.DiagnosticCollection;
constructor() {
super();
this.collection = this._register(vscode.languages.createDiagnosticCollection('markdown'));
}
public override clear(): void {
super.clear();
this.collection.clear();
}
public set(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]): void {
this.collection.set(uri, this.isOpen(uri) ? diagnostics : []);
}
public isOpen(uri: vscode.Uri): boolean {
const tabs = this.getTabResources();
return tabs.has(uri);
}
public delete(uri: vscode.Uri): void {
this.collection.delete(uri);
}
public getOpenDocuments(): ITextDocument[] {
const tabs = this.getTabResources();
return vscode.workspace.textDocuments.filter(doc => tabs.has(doc.uri));
}
private getTabResources(): ResourceMap<void> {
const openedTabDocs = new ResourceMap<void>();
for (const group of vscode.window.tabGroups.all) {
for (const tab of group.tabs) {
if (tab.input instanceof vscode.TabInputText) {
openedTabDocs.set(tab.input.uri);
}
}
}
return openedTabDocs;
}
}
export class DiagnosticManager extends Disposable {
private readonly diagnosticDelayer: Delayer<void>;
private readonly pendingDiagnostics = new Set<vscode.Uri>();
private readonly inFlightDiagnostics = this._register(new InflightDiagnosticRequests());
private readonly linkWatcher = this._register(new LinkWatcher());
private readonly tableOfContentsWatcher: MdTableOfContentsWatcher;
public readonly ready: Promise<void>;
constructor(
private readonly workspace: IMdWorkspace,
private readonly computer: DiagnosticComputer,
private readonly configuration: DiagnosticConfiguration,
private readonly reporter: DiagnosticReporter,
private readonly referencesProvider: MdReferencesProvider,
tocProvider: MdTableOfContentsProvider,
private readonly logger: ILogger,
delay = 300,
) {
super();
this.diagnosticDelayer = this._register(new Delayer(delay));
this._register(this.configuration.onDidChange(() => {
this.rebuild();
}));
this._register(workspace.onDidCreateMarkdownDocument(doc => {
this.triggerDiagnostics(doc.uri);
// Links in other files may have become valid
this.triggerForReferencingFiles(doc.uri);
}));
this._register(workspace.onDidChangeMarkdownDocument(doc => {
this.triggerDiagnostics(doc.uri);
}));
this._register(workspace.onDidDeleteMarkdownDocument(uri => {
this.triggerForReferencingFiles(uri);
}));
this._register(vscode.workspace.onDidCloseTextDocument(({ uri }) => {
this.pendingDiagnostics.delete(uri);
this.inFlightDiagnostics.cancel(uri);
this.linkWatcher.deleteDocument(uri);
this.reporter.delete(uri);
}));
this._register(this.linkWatcher.onDidChangeLinkedToFile(changedDocuments => {
for (const resource of changedDocuments) {
const doc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === resource.toString());
if (doc && isMarkdownFile(doc)) {
this.triggerDiagnostics(doc.uri);
}
}
}));
this.tableOfContentsWatcher = this._register(new MdTableOfContentsWatcher(workspace, tocProvider, delay / 2));
this._register(this.tableOfContentsWatcher.onTocChanged(e => {
return this.triggerForReferencingFiles(e.uri);
}));
this.ready = this.rebuild();
}
private triggerForReferencingFiles(uri: vscode.Uri): Promise<void> {
return this.reporter.addWorkItem(
(async () => {
const triggered = new ResourceMap<Promise<void>>();
for (const ref of await this.referencesProvider.getReferencesToFileInDocs(uri, this.reporter.getOpenDocuments(), noopToken)) {
const file = ref.location.uri;
if (!triggered.has(file)) {
triggered.set(file, this.triggerDiagnostics(file));
}
}
await Promise.all(triggered.values());
})());
}
public override dispose() {
super.dispose();
this.pendingDiagnostics.clear();
}
private async recomputeDiagnosticState(doc: ITextDocument, token: vscode.CancellationToken): Promise<{ diagnostics: readonly vscode.Diagnostic[]; links: readonly MdLink[]; config: DiagnosticOptions }> {
this.logger.verbose('DiagnosticManager', `recomputeDiagnosticState - ${doc.uri}`);
const config = this.configuration.getOptions(doc.uri);
if (!config.enabled) {
return { diagnostics: [], links: [], config };
}
return { ...await this.computer.getDiagnostics(doc, config, token), config };
}
private async recomputePendingDiagnostics(): Promise<void> {
const pending = [...this.pendingDiagnostics];
this.pendingDiagnostics.clear();
await Promise.all(pending.map(async resource => {
const doc = await this.workspace.getOrLoadMarkdownDocument(resource);
if (doc) {
await this.inFlightDiagnostics.trigger(doc.uri, async (token) => {
if (this.reporter.isOpen(doc.uri)) {
const state = await this.recomputeDiagnosticState(doc, token);
this.linkWatcher.updateLinksForDocument(doc.uri, state.config.enabled && state.config.validateFileLinks ? state.links : []);
this.reporter.set(doc.uri, state.diagnostics);
} else {
this.linkWatcher.deleteDocument(doc.uri);
this.reporter.delete(doc.uri);
}
});
}
}));
}
private rebuild(): Promise<void> {
this.reporter.clear();
this.pendingDiagnostics.clear();
this.inFlightDiagnostics.clear();
return this.reporter.addWorkItem(
Promise.all(Array.from(this.reporter.getOpenDocuments(), doc => this.triggerDiagnostics(doc.uri)))
);
}
private async triggerDiagnostics(uri: vscode.Uri): Promise<void> {
this.inFlightDiagnostics.cancel(uri);
this.pendingDiagnostics.add(uri);
return this.reporter.addWorkItem(
this.diagnosticDelayer.trigger(() => this.recomputePendingDiagnostics())
);
}
}
/**
* Map of file paths to markdown links to that file.
*/
class FileLinkMap {
private readonly _filesToLinksMap = new ResourceMap<{
readonly outgoingLinks: Array<{
readonly source: MdLinkSource;
readonly fragment: string;
}>;
}>();
constructor(links: Iterable<MdLink>) {
for (const link of links) {
if (link.href.kind !== 'internal') {
continue;
}
const existingFileEntry = this._filesToLinksMap.get(link.href.path);
const linkData = { source: link.source, fragment: link.href.fragment };
if (existingFileEntry) {
existingFileEntry.outgoingLinks.push(linkData);
} else {
this._filesToLinksMap.set(link.href.path, { outgoingLinks: [linkData] });
}
}
}
public get size(): number {
return this._filesToLinksMap.size;
}
public entries() {
return this._filesToLinksMap.entries();
}
}
export class DiagnosticComputer {
constructor(
private readonly workspace: IMdWorkspace,
private readonly linkProvider: MdLinkProvider,
private readonly tocProvider: MdTableOfContentsProvider,
) { }
public async getDiagnostics(doc: ITextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise<{ readonly diagnostics: vscode.Diagnostic[]; readonly links: readonly MdLink[] }> {
const { links, definitions } = await this.linkProvider.getLinks(doc);
if (token.isCancellationRequested || !options.enabled) {
return { links, diagnostics: [] };
}
return {
links,
diagnostics: (await Promise.all([
this.validateFileLinks(options, links, token),
Array.from(this.validateReferenceLinks(options, links, definitions)),
this.validateFragmentLinks(doc, options, links, token),
])).flat()
};
}
private async validateFragmentLinks(doc: ITextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const severity = toSeverity(options.validateFragmentLinks);
if (typeof severity === 'undefined') {
return [];
}
const toc = await this.tocProvider.getForDocument(doc);
if (token.isCancellationRequested) {
return [];
}
const diagnostics: vscode.Diagnostic[] = [];
for (const link of links) {
if (link.href.kind === 'internal'
&& link.source.hrefText.startsWith('#')
&& link.href.path.toString() === doc.uri.toString()
&& link.href.fragment
&& !toc.lookup(link.href.fragment)
) {
if (!this.isIgnoredLink(options, link.source.hrefText)) {
diagnostics.push(new LinkDoesNotExistDiagnostic(
link.source.hrefRange,
localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment),
severity,
link.source.hrefText));
}
}
}
return diagnostics;
}
private *validateReferenceLinks(options: DiagnosticOptions, links: readonly MdLink[], definitions: LinkDefinitionSet): Iterable<vscode.Diagnostic> {
const severity = toSeverity(options.validateReferences);
if (typeof severity === 'undefined') {
return [];
}
for (const link of links) {
if (link.href.kind === 'reference' && !definitions.lookup(link.href.ref)) {
yield new vscode.Diagnostic(
link.source.hrefRange,
localize('invalidReferenceLink', 'No link definition found: \'{0}\'', link.href.ref),
severity);
}
}
}
private async validateFileLinks(options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const pathErrorSeverity = toSeverity(options.validateFileLinks);
if (typeof pathErrorSeverity === 'undefined') {
return [];
}
const fragmentErrorSeverity = toSeverity(typeof options.validateMarkdownFileLinkFragments === 'undefined' ? options.validateFragmentLinks : options.validateMarkdownFileLinkFragments);
// We've already validated our own fragment links in `validateOwnHeaderLinks`
const linkSet = new FileLinkMap(links.filter(link => !link.source.hrefText.startsWith('#')));
if (linkSet.size === 0) {
return [];
}
const limiter = new Limiter(10);
const diagnostics: vscode.Diagnostic[] = [];
await Promise.all(
Array.from(linkSet.entries()).map(([path, { outgoingLinks: links }]) => {
return limiter.queue(async () => {
if (token.isCancellationRequested) {
return;
}
const resolvedHrefPath = await tryResolveLinkPath(path, this.workspace);
if (!resolvedHrefPath) {
const msg = localize('invalidPathLink', 'File does not exist at path: {0}', path.fsPath);
for (const link of links) {
if (!this.isIgnoredLink(options, link.source.pathText)) {
diagnostics.push(new LinkDoesNotExistDiagnostic(link.source.hrefRange, msg, pathErrorSeverity, link.source.pathText));
}
}
} else if (typeof fragmentErrorSeverity !== 'undefined' && this.isMarkdownPath(resolvedHrefPath)) {
// Validate each of the links to headers in the file
const fragmentLinks = links.filter(x => x.fragment);
if (fragmentLinks.length) {
const toc = await this.tocProvider.get(resolvedHrefPath);
for (const link of fragmentLinks) {
if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.pathText) && !this.isIgnoredLink(options, link.source.hrefText)) {
const msg = localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', link.fragment);
const range = link.source.fragmentRange?.with({ start: link.source.fragmentRange.start.translate(0, -1) }) ?? link.source.hrefRange;
diagnostics.push(new LinkDoesNotExistDiagnostic(range, msg, fragmentErrorSeverity, link.source.hrefText));
}
}
}
}
});
}));
return diagnostics;
}
private isMarkdownPath(resolvedHrefPath: vscode.Uri) {
return this.workspace.hasMarkdownDocument(resolvedHrefPath) || looksLikeMarkdownPath(resolvedHrefPath);
}
private isIgnoredLink(options: DiagnosticOptions, link: string): boolean {
return options.ignoreLinks.some(glob => picomatch.isMatch(link, glob));
}
}
class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
@ -636,17 +47,26 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
const fixes: vscode.CodeAction[] = [];
for (const diagnostic of context.diagnostics) {
if (diagnostic instanceof LinkDoesNotExistDiagnostic) {
const fix = new vscode.CodeAction(
localize('ignoreLinksQuickFix.title', "Exclude '{0}' from link validation.", diagnostic.link),
vscode.CodeActionKind.QuickFix);
switch (diagnostic.code) {
case DiagnosticCode.link_noSuchReferences:
case DiagnosticCode.link_noSuchHeaderInOwnFile:
case DiagnosticCode.link_noSuchFile:
case DiagnosticCode.link_noSuchHeaderInFile: {
const hrefText = (diagnostic as any).data?.hrefText;
if (hrefText) {
const fix = new vscode.CodeAction(
localize('ignoreLinksQuickFix.title', "Exclude '{0}' from link validation.", hrefText),
vscode.CodeActionKind.QuickFix);
fix.command = {
command: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId,
title: '',
arguments: [document.uri, diagnostic.link]
};
fixes.push(fix);
fix.command = {
command: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId,
title: '',
arguments: [document.uri, hrefText],
};
fixes.push(fix);
}
break;
}
}
}
@ -654,26 +74,10 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
}
}
export function registerDiagnosticSupport(
selector: vscode.DocumentSelector,
workspace: IMdWorkspace,
linkProvider: MdLinkProvider,
commandManager: CommandManager,
referenceProvider: MdReferencesProvider,
tocProvider: MdTableOfContentsProvider,
logger: ILogger,
): vscode.Disposable {
const configuration = new VSCodeDiagnosticConfiguration();
const manager = new DiagnosticManager(
workspace,
new DiagnosticComputer(workspace, linkProvider, tocProvider),
configuration,
new DiagnosticCollectionReporter(),
referenceProvider,
tocProvider,
logger);
return vscode.Disposable.from(
configuration,
manager,
AddToIgnoreLinksQuickFixProvider.register(selector, commandManager));
return AddToIgnoreLinksQuickFixProvider.register(selector, commandManager);
}

View file

@ -1,540 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as uri from 'vscode-uri';
import { ILogger } from '../logging';
import { IMdParser } from '../markdownEngine';
import { getLine, ITextDocument } from '../types/textDocument';
import { noopToken } from '../util/cancellation';
import { Disposable } from '../util/dispose';
import { Schemes } from '../util/schemes';
import { MdDocumentInfoCache } from '../util/workspaceCache';
import { IMdWorkspace } from '../workspace';
export interface ExternalHref {
readonly kind: 'external';
readonly uri: vscode.Uri;
}
export interface InternalHref {
readonly kind: 'internal';
readonly path: vscode.Uri;
readonly fragment: string;
}
export interface ReferenceHref {
readonly kind: 'reference';
readonly ref: string;
}
export type LinkHref = ExternalHref | InternalHref | ReferenceHref;
function resolveLink(
document: ITextDocument,
link: string,
): ExternalHref | InternalHref | undefined {
const cleanLink = stripAngleBrackets(link);
if (/^[a-z\-][a-z\-]+:/i.test(cleanLink)) {
// Looks like a uri
return { kind: 'external', uri: vscode.Uri.parse(cleanLink) };
}
// Assume it must be an relative or absolute file path
// Use a fake scheme to avoid parse warnings
const tempUri = vscode.Uri.parse(`vscode-resource:${link}`);
let resourceUri: vscode.Uri | undefined;
if (!tempUri.path) {
resourceUri = document.uri;
} else if (tempUri.path[0] === '/') {
const root = getWorkspaceFolder(document);
if (root) {
resourceUri = vscode.Uri.joinPath(root, tempUri.path);
}
} else {
if (document.uri.scheme === Schemes.untitled) {
const root = getWorkspaceFolder(document);
if (root) {
resourceUri = vscode.Uri.joinPath(root, tempUri.path);
}
} else {
const base = uri.Utils.dirname(document.uri);
resourceUri = vscode.Uri.joinPath(base, tempUri.path);
}
}
if (!resourceUri) {
return undefined;
}
// If we are in a notebook cell, resolve relative to notebook instead
if (resourceUri.scheme === Schemes.notebookCell) {
const notebook = vscode.workspace.notebookDocuments
.find(notebook => notebook.getCells().some(cell => cell.document === document));
if (notebook) {
resourceUri = resourceUri.with({ scheme: notebook.uri.scheme });
}
}
return {
kind: 'internal',
path: resourceUri.with({ fragment: '' }),
fragment: tempUri.fragment,
};
}
function getWorkspaceFolder(document: ITextDocument) {
return vscode.workspace.getWorkspaceFolder(document.uri)?.uri
|| vscode.workspace.workspaceFolders?.[0]?.uri;
}
export interface MdLinkSource {
/**
* The full range of the link.
*/
readonly range: vscode.Range;
/**
* The file where the link is defined.
*/
readonly resource: vscode.Uri;
/**
* The original text of the link destination in code.
*/
readonly hrefText: string;
/**
* The original text of just the link's path in code.
*/
readonly pathText: string;
/**
* The range of the path.
*/
readonly hrefRange: vscode.Range;
/**
* The range of the fragment within the path.
*/
readonly fragmentRange: vscode.Range | undefined;
}
export interface MdInlineLink {
readonly kind: 'link';
readonly source: MdLinkSource;
readonly href: LinkHref;
}
export interface MdLinkDefinition {
readonly kind: 'definition';
readonly source: MdLinkSource;
readonly ref: {
readonly range: vscode.Range;
readonly text: string;
};
readonly href: ExternalHref | InternalHref;
}
export type MdLink = MdInlineLink | MdLinkDefinition;
function extractDocumentLink(
document: ITextDocument,
pre: string,
rawLink: string,
matchIndex: number,
fullMatch: string,
): MdLink | undefined {
const isAngleBracketLink = rawLink.startsWith('<');
const link = stripAngleBrackets(rawLink);
let linkTarget: ExternalHref | InternalHref | undefined;
try {
linkTarget = resolveLink(document, link);
} catch {
return undefined;
}
if (!linkTarget) {
return undefined;
}
const linkStart = document.positionAt(matchIndex);
const linkEnd = linkStart.translate(0, fullMatch.length);
const hrefStart = linkStart.translate(0, pre.length + (isAngleBracketLink ? 1 : 0));
const hrefEnd = hrefStart.translate(0, link.length);
return {
kind: 'link',
href: linkTarget,
source: {
hrefText: link,
resource: document.uri,
range: new vscode.Range(linkStart, linkEnd),
hrefRange: new vscode.Range(hrefStart, hrefEnd),
...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd),
}
};
}
function getFragmentRange(text: string, start: vscode.Position, end: vscode.Position): vscode.Range | undefined {
const index = text.indexOf('#');
if (index < 0) {
return undefined;
}
return new vscode.Range(start.translate({ characterDelta: index + 1 }), end);
}
function getLinkSourceFragmentInfo(document: ITextDocument, link: string, linkStart: vscode.Position, linkEnd: vscode.Position): { fragmentRange: vscode.Range | undefined; pathText: string } {
const fragmentRange = getFragmentRange(link, linkStart, linkEnd);
return {
pathText: document.getText(new vscode.Range(linkStart, fragmentRange ? fragmentRange.start.translate(0, -1) : linkEnd)),
fragmentRange,
};
}
const angleBracketLinkRe = /^<(.*)>$/;
/**
* Used to strip brackets from the markdown link
*
* <http://example.com> will be transformed to http://example.com
*/
function stripAngleBrackets(link: string) {
return link.replace(angleBracketLinkRe, '$1');
}
const r = String.raw;
/**
* Matches `[text](link)` or `[text](<link>)`
*/
const linkPattern = new RegExp(
// text
r`(\[` + // open prefix match -->
/**/r`(?:` +
/*****/r`[^\[\]\\]|` + // Non-bracket chars, or...
/*****/r`\\.|` + // Escaped char, or...
/*****/r`\[[^\[\]]*\]` + // Matched bracket pair
/**/r`)*` +
r`\]` +
// Destination
r`\(\s*)` + // <-- close prefix match
/**/r`(` +
/*****/r`[^\s\(\)\<](?:[^\s\(\)]|\([^\s\(\)]*?\))*|` + // Link without whitespace, or...
/*****/r`<[^<>]+>` + // In angle brackets
/**/r`)` +
// Title
/**/r`\s*(?:"[^"]*"|'[^']*'|\([^\(\)]*\))?\s*` +
r`\)`,
'g');
/**
* Matches `[text][ref]` or `[shorthand]`
*/
const referenceLinkPattern = /(^|[^\]\\])(?:(?:(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]|\[\s*?([^\s\\\]]*?)\])(?![\:\(]))/gm;
/**
* Matches `<http://example.com>`
*/
const autoLinkPattern = /\<(\w+:[^\>\s]+)\>/g;
/**
* Matches `[text]: link`
*/
const definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)([^<]\S*|<[^>]+>)/gm;
const inlineCodePattern = /(?:^|[^`])(`+)(?:.+?|.*?(?:(?:\r?\n).+?)*?)(?:\r?\n)?\1(?:$|[^`])/gm;
class NoLinkRanges {
public static async compute(tokenizer: IMdParser, document: ITextDocument): Promise<NoLinkRanges> {
const tokens = await tokenizer.tokenize(document);
const multiline = tokens.filter(t => (t.type === 'code_block' || t.type === 'fence' || t.type === 'html_block') && !!t.map).map(t => t.map) as [number, number][];
const inlineRanges = new Map</* line number */ number, vscode.Range[]>();
const text = document.getText();
for (const match of text.matchAll(inlineCodePattern)) {
const startOffset = match.index ?? 0;
const startPosition = document.positionAt(startOffset);
const range = new vscode.Range(startPosition, document.positionAt(startOffset + match[0].length));
for (let line = range.start.line; line <= range.end.line; ++line) {
let entry = inlineRanges.get(line);
if (!entry) {
entry = [];
inlineRanges.set(line, entry);
}
entry.push(range);
}
}
return new NoLinkRanges(multiline, inlineRanges);
}
private constructor(
/**
* code blocks and fences each represented by [line_start,line_end).
*/
public readonly multiline: ReadonlyArray<[number, number]>,
/**
* Inline code spans where links should not be detected
*/
public readonly inline: Map</* line number */ number, vscode.Range[]>
) { }
contains(position: vscode.Position): boolean {
return this.multiline.some(interval => position.line >= interval[0] && position.line < interval[1]) ||
!!this.inline.get(position.line)?.some(inlineRange => inlineRange.contains(position));
}
concatInline(inlineRanges: Iterable<vscode.Range>): NoLinkRanges {
const newInline = new Map(this.inline);
for (const range of inlineRanges) {
for (let line = range.start.line; line <= range.end.line; ++line) {
let entry = newInline.get(line);
if (!entry) {
entry = [];
newInline.set(line, entry);
}
entry.push(range);
}
}
return new NoLinkRanges(this.multiline, newInline);
}
}
/**
* Stateless object that extracts link information from markdown files.
*/
export class MdLinkComputer {
constructor(
private readonly tokenizer: IMdParser,
) { }
public async getAllLinks(document: ITextDocument, token: vscode.CancellationToken): Promise<MdLink[]> {
const noLinkRanges = await NoLinkRanges.compute(this.tokenizer, document);
if (token.isCancellationRequested) {
return [];
}
const inlineLinks = Array.from(this.getInlineLinks(document, noLinkRanges));
return Array.from([
...inlineLinks,
...this.getReferenceLinks(document, noLinkRanges.concatInline(inlineLinks.map(x => x.source.range))),
...this.getLinkDefinitions(document, noLinkRanges),
...this.getAutoLinks(document, noLinkRanges),
]);
}
private *getInlineLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
const text = document.getText();
for (const match of text.matchAll(linkPattern)) {
const matchLinkData = extractDocumentLink(document, match[1], match[2], match.index ?? 0, match[0]);
if (matchLinkData && !noLinkRanges.contains(matchLinkData.source.hrefRange.start)) {
yield matchLinkData;
// Also check link destination for links
for (const innerMatch of match[1].matchAll(linkPattern)) {
const innerData = extractDocumentLink(document, innerMatch[1], innerMatch[2], (match.index ?? 0) + (innerMatch.index ?? 0), innerMatch[0]);
if (innerData) {
yield innerData;
}
}
}
}
}
private *getAutoLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
const text = document.getText();
for (const match of text.matchAll(autoLinkPattern)) {
const linkOffset = (match.index ?? 0);
const linkStart = document.positionAt(linkOffset);
if (noLinkRanges.contains(linkStart)) {
continue;
}
const link = match[1];
const linkTarget = resolveLink(document, link);
if (!linkTarget) {
continue;
}
const linkEnd = linkStart.translate(0, match[0].length);
const hrefStart = linkStart.translate(0, 1);
const hrefEnd = hrefStart.translate(0, link.length);
yield {
kind: 'link',
href: linkTarget,
source: {
hrefText: link,
resource: document.uri,
hrefRange: new vscode.Range(hrefStart, hrefEnd),
range: new vscode.Range(linkStart, linkEnd),
...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd),
}
};
}
}
private *getReferenceLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
const text = document.getText();
for (const match of text.matchAll(referenceLinkPattern)) {
const linkStartOffset = (match.index ?? 0) + match[1].length;
const linkStart = document.positionAt(linkStartOffset);
if (noLinkRanges.contains(linkStart)) {
continue;
}
let hrefStart: vscode.Position;
let hrefEnd: vscode.Position;
let reference = match[4];
if (reference === '') { // [ref][],
reference = match[3];
const offset = linkStartOffset + 1;
hrefStart = document.positionAt(offset);
hrefEnd = document.positionAt(offset + reference.length);
} else if (reference) { // [text][ref]
const pre = match[2];
const offset = linkStartOffset + pre.length;
hrefStart = document.positionAt(offset);
hrefEnd = document.positionAt(offset + reference.length);
} else if (match[5]) { // [ref]
reference = match[5];
const offset = linkStartOffset + 1;
hrefStart = document.positionAt(offset);
const line = getLine(document, hrefStart.line);
// See if link looks like a checkbox
const checkboxMatch = line.match(/^\s*[\-\*]\s*\[x\]/i);
if (checkboxMatch && hrefStart.character <= checkboxMatch[0].length) {
continue;
}
hrefEnd = document.positionAt(offset + reference.length);
} else {
continue;
}
const linkEnd = linkStart.translate(0, match[0].length - match[1].length);
yield {
kind: 'link',
source: {
hrefText: reference,
pathText: reference,
resource: document.uri,
range: new vscode.Range(linkStart, linkEnd),
hrefRange: new vscode.Range(hrefStart, hrefEnd),
fragmentRange: undefined,
},
href: {
kind: 'reference',
ref: reference,
}
};
}
}
private *getLinkDefinitions(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLinkDefinition> {
const text = document.getText();
for (const match of text.matchAll(definitionPattern)) {
const offset = (match.index ?? 0);
const linkStart = document.positionAt(offset);
if (noLinkRanges.contains(linkStart)) {
continue;
}
const pre = match[1];
const reference = match[2];
const rawLinkText = match[3].trim();
const target = resolveLink(document, rawLinkText);
if (!target) {
continue;
}
const isAngleBracketLink = angleBracketLinkRe.test(rawLinkText);
const linkText = stripAngleBrackets(rawLinkText);
const hrefStart = linkStart.translate(0, pre.length + (isAngleBracketLink ? 1 : 0));
const hrefEnd = hrefStart.translate(0, linkText.length);
const hrefRange = new vscode.Range(hrefStart, hrefEnd);
const refStart = linkStart.translate(0, 1);
const refRange = new vscode.Range(refStart, refStart.translate({ characterDelta: reference.length }));
const linkEnd = linkStart.translate(0, match[0].length);
yield {
kind: 'definition',
source: {
hrefText: linkText,
resource: document.uri,
range: new vscode.Range(linkStart, linkEnd),
hrefRange,
...getLinkSourceFragmentInfo(document, rawLinkText, hrefStart, hrefEnd),
},
ref: { text: reference, range: refRange },
href: target,
};
}
}
}
interface MdDocumentLinks {
readonly links: readonly MdLink[];
readonly definitions: LinkDefinitionSet;
}
/**
* Stateful object which provides links for markdown files the workspace.
*/
export class MdLinkProvider extends Disposable {
private readonly _linkCache: MdDocumentInfoCache<MdDocumentLinks>;
private readonly linkComputer: MdLinkComputer;
constructor(
tokenizer: IMdParser,
workspace: IMdWorkspace,
logger: ILogger,
) {
super();
this.linkComputer = new MdLinkComputer(tokenizer);
this._linkCache = this._register(new MdDocumentInfoCache(workspace, async doc => {
logger.verbose('LinkProvider', `compute - ${doc.uri}`);
const links = await this.linkComputer.getAllLinks(doc, noopToken);
return {
links,
definitions: new LinkDefinitionSet(links),
};
}));
}
public async getLinks(document: ITextDocument): Promise<MdDocumentLinks> {
return this._linkCache.getForDocument(document);
}
}
export class LinkDefinitionSet implements Iterable<[string, MdLinkDefinition]> {
private readonly _map = new Map<string, MdLinkDefinition>();
constructor(links: Iterable<MdLink>) {
for (const link of links) {
if (link.kind === 'definition') {
this._map.set(link.ref.text, link);
}
}
}
public [Symbol.iterator](): Iterator<[string, MdLinkDefinition]> {
return this._map.entries();
}
public lookup(ref: string): MdLinkDefinition | undefined {
return this._map.get(ref);
}
}

View file

@ -1,329 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as uri from 'vscode-uri';
import { ILogger } from '../logging';
import { IMdParser } from '../markdownEngine';
import { MdTableOfContentsProvider, TocEntry } from '../tableOfContents';
import { ITextDocument } from '../types/textDocument';
import { noopToken } from '../util/cancellation';
import { Disposable } from '../util/dispose';
import { looksLikeMarkdownPath } from '../util/file';
import { MdWorkspaceInfoCache } from '../util/workspaceCache';
import { IMdWorkspace } from '../workspace';
import { InternalHref, MdLink, MdLinkComputer } from './documentLinks';
/**
* A link in a markdown file.
*/
export interface MdLinkReference {
readonly kind: 'link';
readonly isTriggerLocation: boolean;
readonly isDefinition: boolean;
readonly location: vscode.Location;
readonly link: MdLink;
}
/**
* A header in a markdown file.
*/
export interface MdHeaderReference {
readonly kind: 'header';
readonly isTriggerLocation: boolean;
readonly isDefinition: boolean;
/**
* The range of the header.
*
* In `# a b c #` this would be the range of `# a b c #`
*/
readonly location: vscode.Location;
/**
* The text of the header.
*
* In `# a b c #` this would be `a b c`
*/
readonly headerText: string;
/**
* The range of the header text itself.
*
* In `# a b c #` this would be the range of `a b c`
*/
readonly headerTextLocation: vscode.Location;
}
export type MdReference = MdLinkReference | MdHeaderReference;
/**
* Stateful object that computes references for markdown files.
*/
export class MdReferencesProvider extends Disposable {
private readonly _linkCache: MdWorkspaceInfoCache<readonly MdLink[]>;
public constructor(
private readonly parser: IMdParser,
private readonly workspace: IMdWorkspace,
private readonly tocProvider: MdTableOfContentsProvider,
private readonly logger: ILogger,
) {
super();
const linkComputer = new MdLinkComputer(parser);
this._linkCache = this._register(new MdWorkspaceInfoCache(workspace, doc => linkComputer.getAllLinks(doc, noopToken)));
}
public async getReferencesAtPosition(document: ITextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
this.logger.verbose('ReferencesProvider', `getReferencesAtPosition: ${document.uri}`);
const toc = await this.tocProvider.getForDocument(document);
if (token.isCancellationRequested) {
return [];
}
const header = toc.entries.find(entry => entry.line === position.line);
if (header) {
return this.getReferencesToHeader(document, header);
} else {
return this.getReferencesToLinkAtPosition(document, position, token);
}
}
public async getReferencesToFileInWorkspace(resource: vscode.Uri, token: vscode.CancellationToken): Promise<MdReference[]> {
this.logger.verbose('ReferencesProvider', `getAllReferencesToFileInWorkspace: ${resource}`);
const allLinksInWorkspace = (await this._linkCache.values()).flat();
if (token.isCancellationRequested) {
return [];
}
return Array.from(this.findLinksToFile(resource, allLinksInWorkspace, undefined));
}
public async getReferencesToFileInDocs(resource: vscode.Uri, otherDocs: readonly ITextDocument[], token: vscode.CancellationToken): Promise<MdReference[]> {
this.logger.verbose('ReferencesProvider', `getAllReferencesToFileInFiles: ${resource}`);
const links = (await this._linkCache.getForDocs(otherDocs)).flat();
if (token.isCancellationRequested) {
return [];
}
return Array.from(this.findLinksToFile(resource, links, undefined));
}
private async getReferencesToHeader(document: ITextDocument, header: TocEntry): Promise<MdReference[]> {
const links = (await this._linkCache.values()).flat();
const references: MdReference[] = [];
references.push({
kind: 'header',
isTriggerLocation: true,
isDefinition: true,
location: header.headerLocation,
headerText: header.text,
headerTextLocation: header.headerTextLocation
});
for (const link of links) {
if (link.href.kind === 'internal'
&& this.looksLikeLinkToDoc(link.href, document.uri)
&& this.parser.slugifier.fromHeading(link.href.fragment).value === header.slug.value
) {
references.push({
kind: 'link',
isTriggerLocation: false,
isDefinition: false,
link,
location: new vscode.Location(link.source.resource, link.source.hrefRange),
});
}
}
return references;
}
private async getReferencesToLinkAtPosition(document: ITextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
const docLinks = (await this._linkCache.getForDocs([document]))[0];
for (const link of docLinks) {
if (link.kind === 'definition') {
// We could be in either the ref name or the definition
if (link.ref.range.contains(position)) {
return Array.from(this.getReferencesToLinkReference(docLinks, link.ref.text, { resource: document.uri, range: link.ref.range }));
} else if (link.source.hrefRange.contains(position)) {
return this.getReferencesToLink(link, position, token);
}
} else {
if (link.source.hrefRange.contains(position)) {
return this.getReferencesToLink(link, position, token);
}
}
}
return [];
}
private async getReferencesToLink(sourceLink: MdLink, triggerPosition: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
const allLinksInWorkspace = (await this._linkCache.values()).flat();
if (token.isCancellationRequested) {
return [];
}
if (sourceLink.href.kind === 'reference') {
return Array.from(this.getReferencesToLinkReference(allLinksInWorkspace, sourceLink.href.ref, { resource: sourceLink.source.resource, range: sourceLink.source.hrefRange }));
}
if (sourceLink.href.kind === 'external') {
const references: MdReference[] = [];
for (const link of allLinksInWorkspace) {
if (link.href.kind === 'external' && link.href.uri.toString() === sourceLink.href.uri.toString()) {
const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange);
references.push({
kind: 'link',
isTriggerLocation,
isDefinition: false,
link,
location: new vscode.Location(link.source.resource, link.source.hrefRange),
});
}
}
return references;
}
const resolvedResource = await tryResolveLinkPath(sourceLink.href.path, this.workspace);
if (token.isCancellationRequested) {
return [];
}
const references: MdReference[] = [];
if (resolvedResource && this.isMarkdownPath(resolvedResource) && sourceLink.href.fragment && sourceLink.source.fragmentRange?.contains(triggerPosition)) {
const toc = await this.tocProvider.get(resolvedResource);
const entry = toc.lookup(sourceLink.href.fragment);
if (entry) {
references.push({
kind: 'header',
isTriggerLocation: false,
isDefinition: true,
location: entry.headerLocation,
headerText: entry.text,
headerTextLocation: entry.headerTextLocation
});
}
for (const link of allLinksInWorkspace) {
if (link.href.kind !== 'internal' || !this.looksLikeLinkToDoc(link.href, resolvedResource)) {
continue;
}
if (this.parser.slugifier.fromHeading(link.href.fragment).equals(this.parser.slugifier.fromHeading(sourceLink.href.fragment))) {
const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange);
references.push({
kind: 'link',
isTriggerLocation,
isDefinition: false,
link,
location: new vscode.Location(link.source.resource, link.source.hrefRange),
});
}
}
} else { // Triggered on a link without a fragment so we only require matching the file and ignore fragments
references.push(...this.findLinksToFile(resolvedResource ?? sourceLink.href.path, allLinksInWorkspace, sourceLink));
}
return references;
}
private isMarkdownPath(resolvedHrefPath: vscode.Uri) {
return this.workspace.hasMarkdownDocument(resolvedHrefPath) || looksLikeMarkdownPath(resolvedHrefPath);
}
private looksLikeLinkToDoc(href: InternalHref, targetDoc: vscode.Uri) {
return href.path.fsPath === targetDoc.fsPath
|| uri.Utils.extname(href.path) === '' && href.path.with({ path: href.path.path + '.md' }).fsPath === targetDoc.fsPath;
}
private *findLinksToFile(resource: vscode.Uri, links: readonly MdLink[], sourceLink: MdLink | undefined): Iterable<MdReference> {
for (const link of links) {
if (link.href.kind !== 'internal' || !this.looksLikeLinkToDoc(link.href, resource)) {
continue;
}
// Exclude cases where the file is implicitly referencing itself
if (link.source.hrefText.startsWith('#') && link.source.resource.fsPath === resource.fsPath) {
continue;
}
const isTriggerLocation = !!sourceLink && sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange);
const pathRange = this.getPathRange(link);
yield {
kind: 'link',
isTriggerLocation,
isDefinition: false,
link,
location: new vscode.Location(link.source.resource, pathRange),
};
}
}
private *getReferencesToLinkReference(allLinks: Iterable<MdLink>, refToFind: string, from: { resource: vscode.Uri; range: vscode.Range }): Iterable<MdReference> {
for (const link of allLinks) {
let ref: string;
if (link.kind === 'definition') {
ref = link.ref.text;
} else if (link.href.kind === 'reference') {
ref = link.href.ref;
} else {
continue;
}
if (ref === refToFind && link.source.resource.fsPath === from.resource.fsPath) {
const isTriggerLocation = from.resource.fsPath === link.source.resource.fsPath && (
(link.href.kind === 'reference' && from.range.isEqual(link.source.hrefRange)) || (link.kind === 'definition' && from.range.isEqual(link.ref.range)));
const pathRange = this.getPathRange(link);
yield {
kind: 'link',
isTriggerLocation,
isDefinition: link.kind === 'definition',
link,
location: new vscode.Location(from.resource, pathRange),
};
}
}
}
/**
* Get just the range of the file path, dropping the fragment
*/
private getPathRange(link: MdLink): vscode.Range {
return link.source.fragmentRange
? link.source.hrefRange.with(undefined, link.source.fragmentRange.start.translate(0, -1))
: link.source.hrefRange;
}
}
export async function tryResolveLinkPath(originalUri: vscode.Uri, workspace: IMdWorkspace): Promise<vscode.Uri | undefined> {
if (await workspace.pathExists(originalUri)) {
return originalUri;
}
// We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead
if (uri.Utils.extname(originalUri) === '') {
const dotMdResource = originalUri.with({ path: originalUri.path + '.md' });
if (await workspace.pathExists(dotMdResource)) {
return dotMdResource;
}
}
return undefined;
}

View file

@ -70,7 +70,7 @@ export class VsCodeOutputLogger extends Disposable implements ILogger {
}
private readTrace(): Trace {
return Trace.fromString(vscode.workspace.getConfiguration().get<string>('markdown.trace', 'off'));
return Trace.fromString(vscode.workspace.getConfiguration().get<string>('markdown.trace.extension', 'off'));
}
private static data2String(data: any): string {

View file

@ -6,13 +6,23 @@
import Token = require('markdown-it/lib/token');
import { RequestType } from 'vscode-languageclient';
import type * as lsp from 'vscode-languageserver-types';
import type * as md from 'vscode-markdown-languageservice';
// From server
export const parseRequestType: RequestType<{ uri: string }, Token[], any> = new RequestType('markdown/parse');
export const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile');
export const statFileRequestType: RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any> = new RequestType('markdown/statFile');
export const readDirectoryRequestType: RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any> = new RequestType('markdown/readDirectory');
export const findFilesRequestTypes = new RequestType<{}, string[], any>('markdown/findFiles');
//#region From server
export const parse = new RequestType<{ uri: string }, Token[], any>('markdown/parse');
// To server
export const fs_readFile = new RequestType<{ uri: string }, number[], any>('markdown/fs/readFile');
export const fs_readDirectory = new RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any>('markdown/fs/readDirectory');
export const fs_stat = new RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any>('markdown/fs/stat');
export const fs_watcher_create = new RequestType<{ id: number; uri: string; options: md.FileWatcherOptions }, void, any>('markdown/fs/watcher/create');
export const fs_watcher_delete = new RequestType<{ id: number }, void, any>('markdown/fs/watcher/delete');
export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('markdown/findMarkdownFilesInWorkspace');
//#endregion
//#region To server
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');
export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
//#endregion

View file

@ -1,591 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { DiagnosticCollectionReporter, DiagnosticComputer, DiagnosticConfiguration, DiagnosticLevel, DiagnosticManager, DiagnosticOptions, DiagnosticReporter } from '../languageFeatures/diagnostics';
import { MdLinkProvider } from '../languageFeatures/documentLinks';
import { MdReferencesProvider } from '../languageFeatures/references';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { ITextDocument } from '../types/textDocument';
import { noopToken } from '../util/cancellation';
import { DisposableStore } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { ResourceMap } from '../util/resourceMap';
import { IMdWorkspace } from '../workspace';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { assertRangeEqual, joinLines, withStore, workspacePath } from './util';
const defaultDiagnosticsOptions = Object.freeze<DiagnosticOptions>({
enabled: true,
validateFileLinks: DiagnosticLevel.warning,
validateMarkdownFileLinkFragments: undefined,
validateFragmentLinks: DiagnosticLevel.warning,
validateReferences: DiagnosticLevel.warning,
ignoreLinks: [],
});
async function getComputedDiagnostics(store: DisposableStore, doc: InMemoryDocument, workspace: IMdWorkspace, options: Partial<DiagnosticOptions> = {}): Promise<vscode.Diagnostic[]> {
const engine = createNewMarkdownEngine();
const linkProvider = store.add(new MdLinkProvider(engine, workspace, nulLogger));
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const computer = new DiagnosticComputer(workspace, linkProvider, tocProvider);
return (
await computer.getDiagnostics(doc, { ...defaultDiagnosticsOptions, ...options, }, noopToken)
).diagnostics;
}
function assertDiagnosticsEqual(actual: readonly vscode.Diagnostic[], expectedRanges: readonly vscode.Range[]) {
assert.strictEqual(actual.length, expectedRanges.length, "Diagnostic count equal");
for (let i = 0; i < actual.length; ++i) {
assertRangeEqual(actual[i].range, expectedRanges[i], `Range ${i} to be equal`);
}
}
function orderDiagnosticsByRange(diagnostics: Iterable<vscode.Diagnostic>): readonly vscode.Diagnostic[] {
return Array.from(diagnostics).sort((a, b) => a.range.start.compareTo(b.range.start));
}
class MemoryDiagnosticConfiguration implements DiagnosticConfiguration {
private readonly _onDidChange = new vscode.EventEmitter<void>();
public readonly onDidChange = this._onDidChange.event;
private _options: Partial<DiagnosticOptions>;
constructor(options: Partial<DiagnosticOptions>) {
this._options = options;
}
public getOptions(_resource: vscode.Uri): DiagnosticOptions {
return {
...defaultDiagnosticsOptions,
...this._options,
};
}
public update(newOptions: Partial<DiagnosticOptions>) {
this._options = newOptions;
this._onDidChange.fire();
}
}
class MemoryDiagnosticReporter extends DiagnosticReporter {
private readonly diagnostics = new ResourceMap<readonly vscode.Diagnostic[]>();
constructor(
private readonly workspace: InMemoryMdWorkspace,
) {
super();
}
override dispose(): void {
super.clear();
this.clear();
}
override clear(): void {
super.clear();
this.diagnostics.clear();
}
set(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]): void {
this.diagnostics.set(uri, diagnostics);
}
isOpen(_uri: vscode.Uri): boolean {
return true;
}
delete(uri: vscode.Uri): void {
this.diagnostics.delete(uri);
}
get(uri: vscode.Uri): readonly vscode.Diagnostic[] {
return orderDiagnosticsByRange(this.diagnostics.get(uri) ?? []);
}
getOpenDocuments(): ITextDocument[] {
return this.workspace.values();
}
}
suite('markdown: Diagnostic Computer', () => {
test('Should not return any diagnostics for empty document', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`text`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const diagnostics = await getComputedDiagnostics(store, doc, workspace);
assert.deepStrictEqual(diagnostics, []);
}));
test('Should generate diagnostic for link to file that does not exist', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`[bad](/no/such/file.md)`,
`[good](/doc.md)`,
`[good-ref]: /doc.md`,
`[bad-ref]: /no/such/file.md`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const diagnostics = await getComputedDiagnostics(store, doc, workspace);
assertDiagnosticsEqual(diagnostics, [
new vscode.Range(0, 6, 0, 22),
new vscode.Range(3, 11, 3, 27),
]);
}));
test('Should generate diagnostics for links to header that does not exist in current file', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`[good](#good-header)`,
`# Good Header`,
`[bad](#no-such-header)`,
`[good](#good-header)`,
`[good-ref]: #good-header`,
`[bad-ref]: #no-such-header`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const diagnostics = await getComputedDiagnostics(store, doc, workspace);
assertDiagnosticsEqual(diagnostics, [
new vscode.Range(2, 6, 2, 21),
new vscode.Range(5, 11, 5, 26),
]);
}));
test('Should generate diagnostics for links to non-existent headers in other files', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`# My header`,
`[good](#my-header)`,
`[good](/doc1.md#my-header)`,
`[good](doc1.md#my-header)`,
`[good](/doc2.md#other-header)`,
`[bad](/doc2.md#no-such-other-header)`,
));
const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines(
`# Other header`,
));
const diagnostics = await getComputedDiagnostics(store, doc1, new InMemoryMdWorkspace([doc1, doc2]));
assertDiagnosticsEqual(diagnostics, [
new vscode.Range(5, 14, 5, 35),
]);
}));
test('Should support links both with and without .md file extension', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`# My header`,
`[good](#my-header)`,
`[good](/doc.md#my-header)`,
`[good](doc.md#my-header)`,
`[good](/doc#my-header)`,
`[good](doc#my-header)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const diagnostics = await getComputedDiagnostics(store, doc, workspace);
assertDiagnosticsEqual(diagnostics, []);
}));
test('Should generate diagnostics for non-existent link reference', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`[good link][good]`,
`[bad link][no-such]`,
``,
`[good]: http://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const diagnostics = await getComputedDiagnostics(store, doc, workspace);
assertDiagnosticsEqual(diagnostics, [
new vscode.Range(1, 11, 1, 18),
]);
}));
test('Should not generate diagnostics when validate is disabled', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`[text](#no-such-header)`,
`[text][no-such-ref]`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc1]));
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, new MemoryDiagnosticConfiguration({ enabled: false }).getOptions(doc1.uri));
assertDiagnosticsEqual(diagnostics, []);
}));
test('Should not generate diagnostics for email autolink', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`a <user@example.com> c`,
));
const diagnostics = await getComputedDiagnostics(store, doc1, new InMemoryMdWorkspace([doc1]));
assertDiagnosticsEqual(diagnostics, []);
}));
test('Should not generate diagnostics for html tag that looks like an autolink', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`a <tag>b</tag> c`,
`a <scope:tag>b</scope:tag> c`,
));
const diagnostics = await getComputedDiagnostics(store, doc1, new InMemoryMdWorkspace([doc1]));
assertDiagnosticsEqual(diagnostics, []);
}));
test('Should allow ignoring invalid file link using glob', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`[text](/no-such-file)`,
`![img](/no-such-file)`,
`[text]: /no-such-file`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc1]));
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/no-such-file'] });
assertDiagnosticsEqual(diagnostics, []);
}));
test('Should be able to disable fragment validation for external files', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`![i](/doc2.md#no-such)`,
));
const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines(''));
const workspace = new InMemoryMdWorkspace([doc1, doc2]);
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { validateMarkdownFileLinkFragments: DiagnosticLevel.ignore });
assertDiagnosticsEqual(diagnostics, []);
}));
test('Disabling own fragment validation should also disable path fragment validation by default', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`[b](#no-head)`,
`![i](/doc2.md#no-such)`,
));
const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines(''));
const workspace = new InMemoryMdWorkspace([doc1, doc2]);
{
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { validateFragmentLinks: DiagnosticLevel.ignore });
assertDiagnosticsEqual(diagnostics, []);
}
{
// But we should be able to override the default
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { validateFragmentLinks: DiagnosticLevel.ignore, validateMarkdownFileLinkFragments: DiagnosticLevel.warning });
assertDiagnosticsEqual(diagnostics, [
new vscode.Range(1, 13, 1, 21),
]);
}
}));
test('ignoreLinks should allow skipping link to non-existent file', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`[text](/no-such-file#header)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc1]));
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/no-such-file'] });
assertDiagnosticsEqual(diagnostics, []);
}));
test('ignoreLinks should not consider link fragment', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`[text](/no-such-file#header)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc1]));
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/no-such-file'] });
assertDiagnosticsEqual(diagnostics, []);
}));
test('ignoreLinks should support globs', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`![i](/images/aaa.png)`,
`![i](/images/sub/bbb.png)`,
`![i](/images/sub/sub2/ccc.png)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc1]));
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/images/**/*.png'] });
assertDiagnosticsEqual(diagnostics, []);
}));
test('ignoreLinks should support ignoring header', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`![i](#no-such)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc1]));
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['#no-such'] });
assertDiagnosticsEqual(diagnostics, []);
}));
test('ignoreLinks should support ignoring header in file', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`![i](/doc2.md#no-such)`,
));
const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines(''));
const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2]));
{
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/doc2.md#no-such'] });
assertDiagnosticsEqual(diagnostics, []);
}
{
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/doc2.md#*'] });
assertDiagnosticsEqual(diagnostics, []);
}
}));
test('ignoreLinks should support ignore header links if file is ignored', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`![i](/doc2.md#no-such)`,
));
const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines(''));
const workspace = new InMemoryMdWorkspace([doc1, doc2]);
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/doc2.md'] });
assertDiagnosticsEqual(diagnostics, []);
}));
test('Should not detect checkboxes as invalid links', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`- [x]`,
`- [X]`,
`- [ ]`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc1]));
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, { ignoreLinks: ['/doc2.md'] });
assertDiagnosticsEqual(diagnostics, []);
}));
test('Should detect invalid links with titles', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`[link](<no such.md> "text")`,
`[link](<no such.md> 'text')`,
`[link](<no such.md> (text))`,
`[link](no-such.md "text")`,
`[link](no-such.md 'text')`,
`[link](no-such.md (text))`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const diagnostics = await getComputedDiagnostics(store, doc, workspace);
assertDiagnosticsEqual(diagnostics, [
new vscode.Range(0, 8, 0, 18),
new vscode.Range(1, 8, 1, 18),
new vscode.Range(2, 8, 2, 18),
new vscode.Range(3, 7, 3, 17),
new vscode.Range(4, 7, 4, 17),
new vscode.Range(5, 7, 5, 17),
]);
}));
test('Should generate diagnostics for non-existent header using file link to own file', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('sub', 'doc.md'), joinLines(
`[bad](doc.md#no-such)`,
`[bad](doc#no-such)`,
`[bad](/sub/doc.md#no-such)`,
`[bad](/sub/doc#no-such)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const diagnostics = await getComputedDiagnostics(store, doc, workspace);
assertDiagnosticsEqual(orderDiagnosticsByRange(diagnostics), [
new vscode.Range(0, 12, 0, 20),
new vscode.Range(1, 9, 1, 17),
new vscode.Range(2, 17, 2, 25),
new vscode.Range(3, 14, 3, 22),
]);
}));
test('Own header link using file path link should be controlled by "validateMarkdownFileLinkFragments" instead of "validateFragmentLinks"', withStore(async (store) => {
const doc1 = new InMemoryDocument(workspacePath('sub', 'doc.md'), joinLines(
`[bad](doc.md#no-such)`,
`[bad](doc#no-such)`,
`[bad](/sub/doc.md#no-such)`,
`[bad](/sub/doc#no-such)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc1]));
const diagnostics = await getComputedDiagnostics(store, doc1, workspace, {
validateFragmentLinks: DiagnosticLevel.ignore,
validateMarkdownFileLinkFragments: DiagnosticLevel.warning,
});
assertDiagnosticsEqual(orderDiagnosticsByRange(diagnostics), [
new vscode.Range(0, 12, 0, 20),
new vscode.Range(1, 9, 1, 17),
new vscode.Range(2, 17, 2, 25),
new vscode.Range(3, 14, 3, 22),
]);
}));
});
suite('Markdown: Diagnostics manager', () => {
function createDiagnosticsManager(
store: DisposableStore,
workspace: IMdWorkspace,
configuration = new MemoryDiagnosticConfiguration({}),
reporter: DiagnosticReporter = new DiagnosticCollectionReporter(),
) {
const engine = createNewMarkdownEngine();
const linkProvider = store.add(new MdLinkProvider(engine, workspace, nulLogger));
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const referencesProvider = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger));
const manager = store.add(new DiagnosticManager(
workspace,
new DiagnosticComputer(workspace, linkProvider, tocProvider),
configuration,
reporter,
referencesProvider,
tocProvider,
nulLogger,
0));
return manager;
}
test('Changing enable/disable should recompute diagnostics', withStore(async (store) => {
const doc1Uri = workspacePath('doc1.md');
const doc2Uri = workspacePath('doc2.md');
const workspace = store.add(new InMemoryMdWorkspace([
new InMemoryDocument(doc1Uri, joinLines(
`[text](#no-such-1)`,
)),
new InMemoryDocument(doc2Uri, joinLines(
`[text](#no-such-2)`,
))
]));
const reporter = store.add(new MemoryDiagnosticReporter(workspace));
const config = new MemoryDiagnosticConfiguration({ enabled: true });
const manager = createDiagnosticsManager(store, workspace, config, reporter);
await manager.ready;
// Check initial state (Enabled)
await reporter.waitPendingWork();
assertDiagnosticsEqual(reporter.get(doc1Uri), [
new vscode.Range(0, 7, 0, 17),
]);
assertDiagnosticsEqual(reporter.get(doc2Uri), [
new vscode.Range(0, 7, 0, 17),
]);
// Disable
config.update({ enabled: false });
await reporter.waitPendingWork();
assertDiagnosticsEqual(reporter.get(doc1Uri), []);
assertDiagnosticsEqual(reporter.get(doc2Uri), []);
// Enable
config.update({ enabled: true });
await reporter.waitPendingWork();
assertDiagnosticsEqual(reporter.get(doc1Uri), [
new vscode.Range(0, 7, 0, 17),
]);
assertDiagnosticsEqual(reporter.get(doc2Uri), [
new vscode.Range(0, 7, 0, 17),
]);
}));
test('Should revalidate linked files when header changes', withStore(async (store) => {
const doc1Uri = workspacePath('doc1.md');
const doc1 = new InMemoryDocument(doc1Uri, joinLines(
`[text](#no-such)`,
`[text](/doc2.md#header)`,
));
const doc2Uri = workspacePath('doc2.md');
const doc2 = new InMemoryDocument(doc2Uri, joinLines(
`# Header`,
`[text](#header)`,
`[text](#no-such-2)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2]));
const reporter = store.add(new MemoryDiagnosticReporter(workspace));
const manager = createDiagnosticsManager(store, workspace, new MemoryDiagnosticConfiguration({}), reporter);
await manager.ready;
// Check initial state
await reporter.waitPendingWork();
assertDiagnosticsEqual(reporter.get(doc1Uri), [
new vscode.Range(0, 7, 0, 15),
]);
assertDiagnosticsEqual(reporter.get(doc2Uri), [
new vscode.Range(2, 7, 2, 17),
]);
// Edit header
workspace.updateDocument(new InMemoryDocument(doc2Uri, joinLines(
`# new header`,
`[text](#new-header)`,
`[text](#no-such-2)`,
)));
await reporter.waitPendingWork();
assertDiagnosticsEqual(reporter.get(doc1Uri), [
new vscode.Range(0, 7, 0, 15),
new vscode.Range(1, 15, 1, 22),
]);
assertDiagnosticsEqual(reporter.get(doc2Uri), [
new vscode.Range(2, 7, 2, 17),
]);
// Revert to original file
workspace.updateDocument(new InMemoryDocument(doc2Uri, joinLines(
`# header`,
`[text](#header)`,
`[text](#no-such-2)`,
)));
await reporter.waitPendingWork();
assertDiagnosticsEqual(reporter.get(doc1Uri), [
new vscode.Range(0, 7, 0, 15)
]);
assertDiagnosticsEqual(reporter.get(doc2Uri), [
new vscode.Range(2, 7, 2, 17),
]);
}));
test('Should revalidate linked files when file is deleted/created', withStore(async (store) => {
const doc1Uri = workspacePath('doc1.md');
const doc1 = new InMemoryDocument(doc1Uri, joinLines(
`[text](/doc2.md)`,
`[text](/doc2.md#header)`,
));
const doc2Uri = workspacePath('doc2.md');
const doc2 = new InMemoryDocument(doc2Uri, joinLines(
`# Header`
));
const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2]));
const reporter = store.add(new MemoryDiagnosticReporter(workspace));
const manager = createDiagnosticsManager(store, workspace, new MemoryDiagnosticConfiguration({}), reporter);
await manager.ready;
// Check initial state
await reporter.waitPendingWork();
assertDiagnosticsEqual(reporter.get(doc1Uri), []);
// Edit header
workspace.deleteDocument(doc2Uri);
await reporter.waitPendingWork();
assertDiagnosticsEqual(reporter.get(doc1Uri), [
new vscode.Range(0, 7, 0, 15),
new vscode.Range(1, 7, 1, 22),
]);
// Revert to original file
workspace.createDocument(doc2);
await reporter.waitPendingWork();
assertDiagnosticsEqual(reporter.get(doc1Uri), []);
}));
});

View file

@ -1,33 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { MdDocumentInfoCache } from '../util/workspaceCache';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { workspacePath } from './util';
suite('DocumentInfoCache', () => {
test('Repeated calls should only compute value once', async () => {
const doc = workspacePath('doc.md');
const workspace = new InMemoryMdWorkspace([
new InMemoryDocument(doc, '')
]);
let i = 0;
const cache = new MdDocumentInfoCache<number>(workspace, async () => {
return ++i;
});
const a = cache.get(doc);
const b = cache.get(doc);
assert.strictEqual(await a, 1);
assert.strictEqual(i, 1);
assert.strictEqual(await b, 1);
assert.strictEqual(i, 1);
});
});

View file

@ -1,136 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { TableOfContents } from '../tableOfContents';
import { ITextDocument } from '../types/textDocument';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { createNewMarkdownEngine } from './engine';
const testFileName = vscode.Uri.file('test.md');
function createToc(doc: ITextDocument): Promise<TableOfContents> {
const engine = createNewMarkdownEngine();
return TableOfContents.create(engine, doc);
}
suite('markdown.TableOfContentsProvider', () => {
test('Lookup should not return anything for empty document', async () => {
const doc = new InMemoryDocument(testFileName, '');
const provider = await createToc(doc);
assert.strictEqual(provider.lookup(''), undefined);
assert.strictEqual(provider.lookup('foo'), undefined);
});
test('Lookup should not return anything for document with no headers', async () => {
const doc = new InMemoryDocument(testFileName, 'a *b*\nc');
const provider = await createToc(doc);
assert.strictEqual(provider.lookup(''), undefined);
assert.strictEqual(provider.lookup('foo'), undefined);
assert.strictEqual(provider.lookup('a'), undefined);
assert.strictEqual(provider.lookup('b'), undefined);
});
test('Lookup should return basic #header', async () => {
const doc = new InMemoryDocument(testFileName, `# a\nx\n# c`);
const provider = await createToc(doc);
{
const entry = provider.lookup('a');
assert.ok(entry);
assert.strictEqual(entry!.line, 0);
}
{
assert.strictEqual(provider.lookup('x'), undefined);
}
{
const entry = provider.lookup('c');
assert.ok(entry);
assert.strictEqual(entry!.line, 2);
}
});
test('Lookups should be case in-sensitive', async () => {
const doc = new InMemoryDocument(testFileName, `# fOo\n`);
const provider = await createToc(doc);
assert.strictEqual((provider.lookup('fOo'))!.line, 0);
assert.strictEqual((provider.lookup('foo'))!.line, 0);
assert.strictEqual((provider.lookup('FOO'))!.line, 0);
});
test('Lookups should ignore leading and trailing white-space, and collapse internal whitespace', async () => {
const doc = new InMemoryDocument(testFileName, `# f o o \n`);
const provider = await createToc(doc);
assert.strictEqual((provider.lookup('f o o'))!.line, 0);
assert.strictEqual((provider.lookup(' f o o'))!.line, 0);
assert.strictEqual((provider.lookup(' f o o '))!.line, 0);
assert.strictEqual((provider.lookup('f o o'))!.line, 0);
assert.strictEqual((provider.lookup('f o o'))!.line, 0);
assert.strictEqual(provider.lookup('f'), undefined);
assert.strictEqual(provider.lookup('foo'), undefined);
assert.strictEqual(provider.lookup('fo o'), undefined);
});
test('should handle special characters #44779', async () => {
const doc = new InMemoryDocument(testFileName, `# Indentação\n`);
const provider = await createToc(doc);
assert.strictEqual((provider.lookup('indentação'))!.line, 0);
});
test('should handle special characters 2, #48482', async () => {
const doc = new InMemoryDocument(testFileName, `# Инструкция - Делай Раз, Делай Два\n`);
const provider = await createToc(doc);
assert.strictEqual((provider.lookup('инструкция---делай-раз-делай-два'))!.line, 0);
});
test('should handle special characters 3, #37079', async () => {
const doc = new InMemoryDocument(testFileName, `## Header 2
### Header 3
## Заголовок 2
### Заголовок 3
### Заголовок Header 3
## Заголовок`);
const provider = await createToc(doc);
assert.strictEqual((provider.lookup('header-2'))!.line, 0);
assert.strictEqual((provider.lookup('header-3'))!.line, 1);
assert.strictEqual((provider.lookup('Заголовок-2'))!.line, 2);
assert.strictEqual((provider.lookup('Заголовок-3'))!.line, 3);
assert.strictEqual((provider.lookup('Заголовок-header-3'))!.line, 4);
assert.strictEqual((provider.lookup('Заголовок'))!.line, 5);
});
test('Lookup should support suffixes for repeated headers', async () => {
const doc = new InMemoryDocument(testFileName, `# a\n# a\n## a`);
const provider = await createToc(doc);
{
const entry = provider.lookup('a');
assert.ok(entry);
assert.strictEqual(entry!.line, 0);
}
{
const entry = provider.lookup('a-1');
assert.ok(entry);
assert.strictEqual(entry!.line, 1);
}
{
const entry = provider.lookup('a-2');
assert.ok(entry);
assert.strictEqual(entry!.line, 2);
}
});
});

View file

@ -1,89 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { MdTableOfContentsProvider, TableOfContents } from '../tableOfContents';
import { ITextDocument } from '../types/textDocument';
import { IMdWorkspace } from '../workspace';
import { equals } from './arrays';
import { Delayer } from './async';
import { Disposable } from './dispose';
import { ResourceMap } from './resourceMap';
/**
* Check if the items in a table of contents have changed.
*
* This only checks for changes in the entries themselves, not for any changes in their locations.
*/
function hasTableOfContentsChanged(a: TableOfContents, b: TableOfContents): boolean {
const aSlugs = a.entries.map(entry => entry.slug.value).sort();
const bSlugs = b.entries.map(entry => entry.slug.value).sort();
return !equals(aSlugs, bSlugs);
}
export class MdTableOfContentsWatcher extends Disposable {
private readonly _files = new ResourceMap<{
readonly toc: TableOfContents;
}>();
private readonly _pending = new ResourceMap<void>();
private readonly _onTocChanged = this._register(new vscode.EventEmitter<{ readonly uri: vscode.Uri }>);
public readonly onTocChanged = this._onTocChanged.event;
private readonly delayer: Delayer<void>;
public constructor(
private readonly workspace: IMdWorkspace,
private readonly tocProvider: MdTableOfContentsProvider,
private readonly delay: number,
) {
super();
this.delayer = this._register(new Delayer<void>(delay));
this._register(this.workspace.onDidChangeMarkdownDocument(this.onDidChangeDocument, this));
this._register(this.workspace.onDidCreateMarkdownDocument(this.onDidCreateDocument, this));
this._register(this.workspace.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this));
}
private async onDidCreateDocument(document: ITextDocument) {
const toc = await this.tocProvider.getForDocument(document);
this._files.set(document.uri, { toc });
}
private async onDidChangeDocument(document: ITextDocument) {
if (this.delay > 0) {
this._pending.set(document.uri);
this.delayer.trigger(() => this.flushPending());
} else {
this.updateForResource(document.uri);
}
}
private onDidDeleteDocument(resource: vscode.Uri) {
this._files.delete(resource);
this._pending.delete(resource);
}
private async flushPending() {
const pending = [...this._pending.keys()];
this._pending.clear();
return Promise.all(pending.map(resource => this.updateForResource(resource)));
}
private async updateForResource(resource: vscode.Uri) {
const existing = this._files.get(resource);
const newToc = await this.tocProvider.get(resource);
if (!existing || hasTableOfContentsChanged(existing.toc, newToc)) {
this._onTocChanged.fire({ uri: resource });
}
this._files.set(resource, { toc: newToc });
}
}

View file

@ -114,74 +114,3 @@ export class MdDocumentInfoCache<T> extends Disposable {
this._cache.delete(resource);
}
}
/**
* Cache of information across all markdown files in the workspace.
*
* Unlike {@link MdDocumentInfoCache}, the entries here are computed eagerly for every file in the workspace.
* However the computation of the values is still lazy.
*/
export class MdWorkspaceInfoCache<T> extends Disposable {
private readonly _cache = new LazyResourceMap<T>();
private _init?: Promise<void>;
public constructor(
private readonly workspace: IMdWorkspace,
private readonly getValue: (document: ITextDocument) => Promise<T>,
) {
super();
}
public async entries(): Promise<Array<[vscode.Uri, T]>> {
await this.ensureInit();
return this._cache.entries();
}
public async values(): Promise<Array<T>> {
await this.ensureInit();
return Array.from(await this._cache.entries(), x => x[1]);
}
public async getForDocs(docs: readonly ITextDocument[]): Promise<T[]> {
for (const doc of docs) {
if (!this._cache.has(doc.uri)) {
this.update(doc);
}
}
return Promise.all(docs.map(doc => this._cache.get(doc.uri) as Promise<T>));
}
private async ensureInit(): Promise<void> {
if (!this._init) {
this._init = this.populateCache();
this._register(this.workspace.onDidChangeMarkdownDocument(this.onDidChangeDocument, this));
this._register(this.workspace.onDidCreateMarkdownDocument(this.onDidChangeDocument, this));
this._register(this.workspace.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this));
}
await this._init;
}
private async populateCache(): Promise<void> {
const markdownDocumentUris = await this.workspace.getAllMarkdownDocuments();
for (const document of markdownDocumentUris) {
if (!this._cache.has(document.uri)) {
this.update(document);
}
}
}
private update(document: ITextDocument): void {
this._cache.set(document.uri, lazy(() => this.getValue(document)));
}
private onDidChangeDocument(document: ITextDocument) {
this.update(document);
}
private onDidDeleteDocument(resource: vscode.Uri) {
this._cache.delete(resource);
}
}

View file

@ -237,21 +237,42 @@ vscode-languageserver-textdocument@^1.0.4:
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz#3cd56dd14cec1d09e86c4bb04b09a246cb3df157"
integrity sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ==
vscode-languageserver-textdocument@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.5.tgz#838769940ece626176ec5d5a2aa2d0aa69f5095c"
integrity sha512-1ah7zyQjKBudnMiHbZmxz5bYNM9KKZYz+5VQLj+yr8l+9w3g+WAhCkUkWbhMEdC5u0ub4Ndiye/fDyS8ghIKQg==
vscode-languageserver-types@3.17.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz#c2d87fa7784f8cac389deb3ff1e2d9a7bef07e16"
integrity sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ==
vscode-languageserver-types@^3.17.2:
vscode-languageserver-types@^3.17.1, vscode-languageserver-types@^3.17.2:
version "3.17.2"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz#b2c2e7de405ad3d73a883e91989b850170ffc4f2"
integrity sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==
vscode-markdown-languageservice@^0.0.0-alpha.10:
version "0.0.0-alpha.10"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.0.0-alpha.10.tgz#53b69c981eed7fd5efa155ab8c0f169995568681"
integrity sha512-rJ85nJ+d45yCz9lBhipavoWXz/vW5FknqqUpLqhe3/2xkrhxt8zcekhSoDepgkKFcTORAFV6g1SnnqxbVhX+uA==
dependencies:
picomatch "^2.3.1"
vscode-languageserver-textdocument "^1.0.5"
vscode-languageserver-types "^3.17.1"
vscode-nls "^5.0.1"
vscode-uri "^3.0.3"
vscode-nls@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840"
integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA==
vscode-nls@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.1.tgz#ba23fc4d4420d25e7f886c8e83cbdcec47aa48b2"
integrity sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A==
vscode-uri@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84"

View file

@ -554,8 +554,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType
}
// Reconfigure any plugins
for (const [config, pluginName] of this.pluginManager.configurations()) {
this.configurePlugin(config, pluginName);
for (const [pluginName, config] of this.pluginManager.configurations()) {
this.configurePlugin(pluginName, config);
}
}

View file

@ -258,7 +258,7 @@ const apiTestContentProvider: vscode.NotebookContentProvider = {
// });
});
test('edit API batch edits', async function () {
test.skip('edit API batch edits', async function () { // TODO@rebornix https://github.com/microsoft/vscode/issues/155808
const notebook = await openRandomNotebookDocument();
const editor = await vscode.window.showNotebookDocument(notebook);
@ -284,7 +284,7 @@ const apiTestContentProvider: vscode.NotebookContentProvider = {
assert.ok(cell.metadata.extraCellMetadata, `Test cell metdata not found`);
});
test('edit API batch edits undo/redo', async function () {
test.skip('edit API batch edits undo/redo', async function () { // TODO@rebornix https://github.com/microsoft/vscode/issues/155825
const notebook = await openRandomNotebookDocument();
const editor = await vscode.window.showNotebookDocument(notebook);

View file

@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.70.0",
"distro": "1a72c46622967eab6ea48516a2153c55d7e18e53",
"distro": "990065ff739688c0d4ad94e172eeccd6bd5ef124",
"author": {
"name": "Microsoft Corporation"
},
@ -56,7 +56,8 @@
"minify-vscode-reh-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode-reh-web",
"hygiene": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js hygiene",
"core-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js core-ci",
"extensions-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci"
"extensions-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci",
"webview-generate-csp-hash": "npx github:apaatsio/csp-hash-from-html csp-hash ./src/vs/workbench/contrib/webview/browser/pre/index.html"
},
"dependencies": {
"@microsoft/1ds-core-js": "^3.2.2",
@ -85,12 +86,12 @@
"vscode-proxy-agent": "^0.12.0",
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "7.0.1",
"xterm": "4.20.0-beta.13",
"xterm-addon-search": "0.10.0-beta.2",
"xterm-addon-serialize": "0.8.0-beta.2",
"xterm": "4.20.0-beta.20",
"xterm-addon-search": "0.10.0-beta.3",
"xterm-addon-serialize": "0.8.0-beta.3",
"xterm-addon-unicode11": "0.4.0-beta.3",
"xterm-addon-webgl": "0.13.0-beta.7",
"xterm-headless": "4.20.0-beta.13",
"xterm-addon-webgl": "0.13.0-beta.9",
"xterm-headless": "4.20.0-beta.20",
"yauzl": "^2.9.2",
"yazl": "^2.4.3"
},
@ -124,7 +125,7 @@
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"@vscode/telemetry-extractor": "^1.9.6",
"@vscode/test-web": "^0.0.22",
"@vscode/test-web": "^0.0.29",
"ansi-colors": "^3.2.3",
"asar": "^3.0.3",
"chromium-pickle-js": "^0.2.0",
@ -199,7 +200,7 @@
"style-loader": "^1.3.0",
"ts-loader": "^9.2.7",
"tsec": "0.1.4",
"typescript": "^4.8.0-dev.20220711",
"typescript": "^4.8.0-dev.20220719",
"typescript-formatter": "7.1.0",
"underscore": "^1.12.1",
"util": "^0.12.4",

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

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

View file

@ -68,22 +68,22 @@ vscode-textmate@7.0.1:
resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-7.0.1.tgz#8118a32b02735dccd14f893b495fa5389ad7de79"
integrity sha512-zQ5U/nuXAAMsh691FtV0wPz89nSkHbs+IQV8FDk+wew9BlSDhf4UmWGlWJfTR2Ti6xZv87Tj5fENzKf6Qk7aLw==
xterm-addon-search@0.10.0-beta.2:
version "0.10.0-beta.2"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.2.tgz#a937d1e9a70fde8eeb7d1df485039b2d5fc1d707"
integrity sha512-ybafAbX9V4sfkzmUsWmtfEYExG8jj73bTF9pEa/Lhd5q4bviW4LcFaw/n3lKHn/1tSgSVgzoD13u1ZaZR78SfQ==
xterm-addon-search@0.10.0-beta.3:
version "0.10.0-beta.3"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.3.tgz#5194434d86105637c71f6f20139a9d0b5c1a956a"
integrity sha512-UeGm/ymnp7HUYJJtsP0D+bljOWbdk3MctcLJ+0jv8AmU6YlAzJFtouvYSQrD5SAMyht5CRsvjzFgqic9X02JYg==
xterm-addon-unicode11@0.4.0-beta.3:
version "0.4.0-beta.3"
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa"
integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q==
xterm-addon-webgl@0.13.0-beta.7:
version "0.13.0-beta.7"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.7.tgz#9b514fa9792ced755c8321ddd06529f9912db2a1"
integrity sha512-U3sKzkziZRwb13MyNp0eMwt5BWyM938epAv0ZOnI5Vjq06S2naS37GgO/FY+0eu5wuBKkZhT9lNDv7n8rMqd+w==
xterm-addon-webgl@0.13.0-beta.9:
version "0.13.0-beta.9"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.9.tgz#66a9ac142ae347d0548abbf4e66bb2f35f415adb"
integrity sha512-x1o1tpCqIsICvhcRsZs+BLcwUIdizYS2G4TIH0KBnUDiSN+oSqpVBQNG8qKg56xbK8WtpdbQ9dLB7JR2W5cX0g==
xterm@4.20.0-beta.13:
version "4.20.0-beta.13"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.13.tgz#7526e19310daa56a11c26eb81a2706593c1378ec"
integrity sha512-ovXGhvM/qDRNGlGO653WY1pxIZrnI4+ayVM7pCnxO/tRwUIym6LGS3NurYrhGYrXOjoLJZxQ4EjToAfHvAg2Gg==
xterm@4.20.0-beta.20:
version "4.20.0-beta.20"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.20.tgz#2979a31839f7b8ee3ffe4f063b40c02facdb0fed"
integrity sha512-ltDtTquH+33tXQPFSDqenbgz6LkvIob6l6Rac85L4aX5Ve7P3ubVLrq+lTFJGQn3iiwGqNmnE1t1EUuGhxsXcQ==

View file

@ -788,35 +788,35 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
xterm-addon-search@0.10.0-beta.2:
version "0.10.0-beta.2"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.2.tgz#a937d1e9a70fde8eeb7d1df485039b2d5fc1d707"
integrity sha512-ybafAbX9V4sfkzmUsWmtfEYExG8jj73bTF9pEa/Lhd5q4bviW4LcFaw/n3lKHn/1tSgSVgzoD13u1ZaZR78SfQ==
xterm-addon-search@0.10.0-beta.3:
version "0.10.0-beta.3"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0-beta.3.tgz#5194434d86105637c71f6f20139a9d0b5c1a956a"
integrity sha512-UeGm/ymnp7HUYJJtsP0D+bljOWbdk3MctcLJ+0jv8AmU6YlAzJFtouvYSQrD5SAMyht5CRsvjzFgqic9X02JYg==
xterm-addon-serialize@0.8.0-beta.2:
version "0.8.0-beta.2"
resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0-beta.2.tgz#f30656d4ff1570ac105bacffe443385666654598"
integrity sha512-IDaRxO1zwjF9fDJp6u27Lv8852kEZ0HlbB0wLZbcIGZxDuPDLfvw8s/BV7f6MFB+mZq19CjyHGH4oPzZkc0rLQ==
xterm-addon-serialize@0.8.0-beta.3:
version "0.8.0-beta.3"
resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0-beta.3.tgz#47ade3fedacbb75bd26e63cfe0120586623e0e4f"
integrity sha512-gvfempZCYuAhLqN4O6fA2TuoavPjOxFKlh8hLcOzPackiLUhwKr1jQpDXcnq8VgqUiGgb+XNZpPEbI0Q7EhTgA==
xterm-addon-unicode11@0.4.0-beta.3:
version "0.4.0-beta.3"
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa"
integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q==
xterm-addon-webgl@0.13.0-beta.7:
version "0.13.0-beta.7"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.7.tgz#9b514fa9792ced755c8321ddd06529f9912db2a1"
integrity sha512-U3sKzkziZRwb13MyNp0eMwt5BWyM938epAv0ZOnI5Vjq06S2naS37GgO/FY+0eu5wuBKkZhT9lNDv7n8rMqd+w==
xterm-addon-webgl@0.13.0-beta.9:
version "0.13.0-beta.9"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0-beta.9.tgz#66a9ac142ae347d0548abbf4e66bb2f35f415adb"
integrity sha512-x1o1tpCqIsICvhcRsZs+BLcwUIdizYS2G4TIH0KBnUDiSN+oSqpVBQNG8qKg56xbK8WtpdbQ9dLB7JR2W5cX0g==
xterm-headless@4.20.0-beta.13:
version "4.20.0-beta.13"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.13.tgz#a7d9d8837e3f78e106006cc94cf63ec13a9fd991"
integrity sha512-y4YI+Ogv2R2I++tsyvx5Q7csAaN7mG2yMMMBb/u4dXnrFmSGYs/R8ZFkeHgAW4Ju4uI3Rizb+ZdwtN1uG043Rw==
xterm-headless@4.20.0-beta.20:
version "4.20.0-beta.20"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.20.0-beta.20.tgz#da2d8131b02d6f1e37f47cc17e578f2c2980fbb6"
integrity sha512-JK4jUIiUH7TdzvMrpfDnbGxTuC4s7byjqnMHR8+gIpY8qCFjz0xcMFSbp+ZshxGwVyziI4jtJqTHZjFToT2/kw==
xterm@4.20.0-beta.13:
version "4.20.0-beta.13"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.13.tgz#7526e19310daa56a11c26eb81a2706593c1378ec"
integrity sha512-ovXGhvM/qDRNGlGO653WY1pxIZrnI4+ayVM7pCnxO/tRwUIym6LGS3NurYrhGYrXOjoLJZxQ4EjToAfHvAg2Gg==
xterm@4.20.0-beta.20:
version "4.20.0-beta.20"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.20.0-beta.20.tgz#2979a31839f7b8ee3ffe4f063b40c02facdb0fed"
integrity sha512-ltDtTquH+33tXQPFSDqenbgz6LkvIob6l6Rac85L4aX5Ve7P3ubVLrq+lTFJGQn3iiwGqNmnE1t1EUuGhxsXcQ==
yallist@^4.0.0:
version "4.0.0"

View file

@ -1,7 +1,8 @@
Package: @@NAME@@
Version: @@VERSION@@
Section: devel
Depends: libnss3 (>= 2:3.26), gnupg, apt, libxkbfile1, libsecret-1-0, libgtk-3-0 (>= 3.10.0), libxss1, libgbm1
Depends: @@DEPENDS@@
Recommends: @@RECOMMENDS@@
Priority: optional
Architecture: @@ARCHITECTURE@@
Maintainer: Microsoft Corporation <vscode-linux@microsoft.com>

View file

@ -1738,56 +1738,81 @@ type HTMLElementAttributeKeys<T> = Partial<{ [K in keyof T]: T[K] extends Functi
type ElementAttributes<T> = HTMLElementAttributeKeys<T> & Record<string, any>;
type RemoveHTMLElement<T> = T extends HTMLElement ? never : T;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type ArrayToObj<T extends any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
type ArrayToObj<T extends readonly any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
type HHTMLElementTagNameMap = HTMLElementTagNameMap & { '': HTMLDivElement };
type TagToElement<T> = T extends `.${string}`
? HTMLDivElement
: T extends `#${string}`
? HTMLDivElement
: T extends `${infer TStart}#${string}`
? TStart extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[TStart]
type TagToElement<T> = T extends `${infer TStart}#${string}`
? TStart extends keyof HHTMLElementTagNameMap
? HHTMLElementTagNameMap[TStart]
: HTMLElement
: T extends `${infer TStart}.${string}`
? TStart extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[TStart]
? TStart extends keyof HHTMLElementTagNameMap
? HHTMLElementTagNameMap[TStart]
: HTMLElement
: T extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[T]
: HTMLElement;
type TagToElementAndId<TTag> = TTag extends `${infer TTag}@${infer TId}`
? { element: TagToElement<TTag>; id: TId }
: { element: TagToElement<TTag>; id: 'root' };
type TagToRecord<TTag> = TagToElementAndId<TTag> extends { element: infer TElement; id: infer TId }
? Record<(TId extends string ? TId : never) | 'root', TElement>
: never;
type Child = HTMLElement | string | Record<string, HTMLElement>;
type Children = []
| [Child]
| [Child, Child]
| [Child, Child, Child]
| [Child, Child, Child, Child]
| [Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child]
| [Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child, Child];
const H_REGEX = /(?<tag>[\w\-]+)?(?:#(?<id>[\w\-]+))?(?<class>(?:\.(?:[\w\-]+))*)(?:@(?<name>(?:[\w\_])+))?/;
/**
* A helper function to create nested dom nodes.
*
*
* ```ts
* private readonly htmlElements = h('div.code-view', [
* h('div.title', { $: 'title' }),
* const elements = h('div.code-view', [
* h('div.title@title'),
* h('div.container', [
* h('div.gutter', { $: 'gutterDiv' }),
* h('div', { $: 'editor' }),
* h('div.gutter@gutterDiv'),
* h('div@editor'),
* ]),
* ]);
* private readonly editor = createEditor(this.htmlElements.editor);
* const editor = createEditor(elements.editor);
* ```
*/
export function h<TTag extends string>(
tag: TTag
): (Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string, TId extends string>(
tag: TTag,
attributes: { $: TId } & Partial<ElementAttributes<TagToElement<TTag>>>
): Record<TId | 'root', TagToElement<TTag>>;
export function h<TTag extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>(
tag: TTag,
children: T
): (ArrayToObj<T> & Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string>(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>): Record<'root', TagToElement<TTag>>;
export function h<TTag extends string, TId extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>(
tag: TTag,
attributes: { $: TId } & Partial<ElementAttributes<TagToElement<TTag>>>,
children: T
): (ArrayToObj<T> & Record<TId, TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string>
(tag: TTag):
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string, T extends Children>
(tag: TTag, children: T):
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string>
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>):
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string, T extends Children>
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: T):
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>;
let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined;
@ -1800,25 +1825,29 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia
children = args[1];
}
const match = SELECTOR_REGEX.exec(tag);
const match = H_REGEX.exec(tag);
if (!match) {
if (!match || !match.groups) {
throw new Error('Bad use of h');
}
const tagName = match[1] || 'div';
const tagName = match.groups['tag'] || 'div';
const el = document.createElement(tagName);
if (match[3]) {
el.id = match[3];
if (match.groups['id']) {
el.id = match.groups['id'];
}
if (match[4]) {
el.className = match[4].replace(/\./g, ' ').trim();
if (match.groups['class']) {
el.className = match.groups['class'].replace(/\./g, ' ').trim();
}
const result: Record<string, HTMLElement> = {};
if (match.groups['name']) {
result[match.groups['name']] = el;
}
if (children) {
for (const c of children) {
if (c instanceof HTMLElement) {
@ -1833,10 +1862,6 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia
}
for (const [key, value] of Object.entries(attributes)) {
if (key === '$') {
result[value] = el;
continue;
}
if (key === 'style') {
for (const [cssKey, cssValue] of Object.entries(value)) {
el.style.setProperty(

View file

@ -9,8 +9,6 @@ import { DomEmitter } from 'vs/base/browser/event';
import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { raceCancellation } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Event } from 'vs/base/common/event';
import { IMarkdownString, escapeDoubleQuotes, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
@ -44,8 +42,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
const disposables = new DisposableStore();
let isDisposed = false;
const cts = disposables.add(new CancellationTokenSource());
const element = createElement(options);
const _uriMassage = function (part: string): string {
@ -96,11 +92,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
return uri.toString();
};
// signal to code-block render that the
// element has been created
let signalInnerHTML: () => void;
const withInnerHTML = new Promise<void>(c => signalInnerHTML = c);
const renderer = new marked.Renderer();
renderer.image = (href: string, title: string, text: string) => {
@ -146,24 +137,14 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
return `<p>${text}</p>`;
};
// Will collect [id, renderedElement] tuples
const codeBlocks: Promise<[string, HTMLElement]>[] = [];
if (options.codeBlockRenderer) {
renderer.code = (code, lang) => {
const value = options.codeBlockRenderer!(lang ?? '', code);
// when code-block rendering is async we return sync
// but update the node with the real result later.
const id = defaultGenerator.nextId();
raceCancellation(Promise.all([value, withInnerHTML]), cts.token).then(values => {
if (!isDisposed && values) {
const span = element.querySelector<HTMLDivElement>(`div[data-code="${id}"]`);
if (span) {
DOM.reset(span, values[0]);
}
options.asyncRenderCallback?.();
}
}).catch(() => {
// ignore
});
const value = options.codeBlockRenderer!(lang ?? '', code);
codeBlocks.push(value.then(element => [id, element]));
return `<div class="code" data-code="${id}">${escape(code)}</div>`;
};
}
@ -277,8 +258,22 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
element.innerHTML = sanitizeRenderedMarkdown(markdown, markdownHtmlDoc.body.innerHTML) as unknown as string;
// signal that async code blocks can be now be inserted
signalInnerHTML!();
if (codeBlocks.length > 0) {
Promise.all(codeBlocks).then((tuples) => {
if (isDisposed) {
return;
}
const renderedElements = new Map(tuples);
const placeholderElements = element.querySelectorAll<HTMLDivElement>(`div[data-code]`);
for (const placeholderElement of placeholderElements) {
const renderedElement = renderedElements.get(placeholderElement.dataset['code'] ?? '');
if (renderedElement) {
DOM.reset(placeholderElement, renderedElement);
}
}
options.asyncRenderCallback?.();
});
}
// signal size changes for image tags
if (options.asyncRenderCallback) {
@ -294,7 +289,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
element,
dispose: () => {
isDisposed = true;
cts.cancel();
disposables.dispose();
}
};

View file

@ -49,7 +49,11 @@
.monaco-button-dropdown.disabled > .monaco-button.disabled,
.monaco-button-dropdown.disabled > .monaco-button.disabled:focus,
.monaco-button-dropdown.disabled > .monaco-button-dropdown-separator {
opacity: 0.4;
opacity: 0.4 !important;
}
.monaco-button-dropdown > .monaco-button.monaco-text-button {
border-right-width: 0 !important;
}
.monaco-button-dropdown .monaco-button-dropdown-separator {
@ -62,6 +66,10 @@
width: 1px;
}
.monaco-button-dropdown > .monaco-button.monaco-dropdown-button {
border-left-width: 0 !important;
}
.monaco-description-button {
flex-direction: column;
}

View file

@ -316,6 +316,16 @@ export class ButtonWithDropdown extends Disposable implements IButton {
this.dropdownButton.style(styles);
// Separator
const border = styles.buttonBorder ? styles.buttonBorder.toString() : '';
this.separatorContainer.style.borderTopWidth = border ? '1px' : '';
this.separatorContainer.style.borderTopStyle = border ? 'solid' : '';
this.separatorContainer.style.borderTopColor = border;
this.separatorContainer.style.borderBottomWidth = border ? '1px' : '';
this.separatorContainer.style.borderBottomStyle = border ? 'solid' : '';
this.separatorContainer.style.borderBottomColor = border;
this.separatorContainer.style.backgroundColor = styles.buttonBackground?.toString() ?? '';
this.separator.style.backgroundColor = styles.buttonSeparator?.toString() ?? '';
}

View file

@ -424,6 +424,7 @@ class TypeNavigationController<T> implements IDisposable {
private list: List<T>,
private view: ListView<T>,
private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider<T>,
private keyboardNavigationEventFilter: IKeyboardNavigationEventFilter,
private delegate: IKeyboardNavigationDelegate
) {
this.updateOptions(list.options);
@ -448,12 +449,15 @@ class TypeNavigationController<T> implements IDisposable {
return;
}
let typing = false;
const onChar = this.enabledDisposables.add(Event.chain(this.enabledDisposables.add(new DomEmitter(this.view.domNode, 'keydown')).event))
.filter(e => !isInputElement(e.target as HTMLElement))
.filter(() => this.mode === TypeNavigationMode.Automatic || this.triggered)
.map(event => new StandardKeyboardEvent(event))
.filter(e => typing || this.keyboardNavigationEventFilter(e))
.filter(e => this.delegate.mightProducePrintableCharacter(e))
.forEach(e => { e.preventDefault(); e.stopPropagation(); })
.forEach(stopEvent)
.map(event => event.browserEvent.key)
.event;
@ -463,6 +467,9 @@ class TypeNavigationController<T> implements IDisposable {
onInput(this.onInput, this, this.enabledDisposables);
onClear(this.onClear, this, this.enabledDisposables);
onChar(() => typing = true, undefined, this.enabledDisposables);
onClear(() => typing = false, undefined, this.enabledDisposables);
this.enabled = true;
this.triggered = false;
}
@ -919,6 +926,10 @@ export class DefaultStyleController implements IStyleController {
}
}
export interface IKeyboardNavigationEventFilter {
(e: StandardKeyboardEvent): boolean;
}
export interface IListOptionsUpdate extends IListViewOptionsUpdate {
readonly typeNavigationEnabled?: boolean;
readonly typeNavigationMode?: TypeNavigationMode;
@ -934,6 +945,7 @@ export interface IListOptions<T> extends IListOptionsUpdate {
readonly multipleSelectionController?: IMultipleSelectionController<T>;
readonly styleController?: (suffix: string) => IStyleController;
readonly accessibilityProvider?: IListAccessibilityProvider<T>;
readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
// list view options
readonly useShadows?: boolean;
@ -1373,7 +1385,7 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
if (_options.keyboardNavigationLabelProvider) {
const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate;
this.typeNavigationController = new TypeNavigationController(this, this.view, _options.keyboardNavigationLabelProvider, delegate);
this.typeNavigationController = new TypeNavigationController(this, this.view, _options.keyboardNavigationLabelProvider, _options.keyboardNavigationEventFilter ?? (() => true), delegate);
this.disposables.add(this.typeNavigationController);
}
@ -1785,6 +1797,10 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
return this.view.domNode;
}
getElementID(index: number): string {
return this.view.getElementDomId(index);
}
style(styles: IListStyles): void {
this.styleController.style(styles);
}

View file

@ -684,10 +684,10 @@ export enum TreeFindMode {
class FindWidget<T, TFilterData> extends Disposable {
private readonly elements = h('div.monaco-tree-type-filter', [
h('div.monaco-tree-type-filter-grab.codicon.codicon-debug-gripper', { $: 'grab' }),
h('div.monaco-tree-type-filter-input', { $: 'findInput' }),
h('div.monaco-tree-type-filter-actionbar', { $: 'actionbar' }),
private readonly elements = h('.monaco-tree-type-filter', [
h('.monaco-tree-type-filter-grab.codicon.codicon-debug-gripper@grab'),
h('.monaco-tree-type-filter-input@findInput'),
h('.monaco-tree-type-filter-actionbar@actionbar'),
]);
set mode(mode: TreeFindMode) {
@ -1003,10 +1003,6 @@ function asTreeContextMenuEvent<T>(event: IListContextMenuEvent<ITreeNode<T, any
};
}
export interface IKeyboardNavigationEventFilter {
(e: StandardKeyboardEvent): boolean;
}
export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions {
readonly multipleSelectionSupport?: boolean;
readonly typeNavigationEnabled?: boolean;
@ -1025,7 +1021,6 @@ export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTr
readonly collapseByDefault?: boolean; // defaults to false
readonly filter?: ITreeFilter<T, TFilterData>;
readonly dnd?: ITreeDragAndDrop<T>;
readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
readonly additionalScrollHeight?: number;
}

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