diff --git a/.eslint-ignore b/.eslint-ignore index 91b5117c20a..da66a76c396 100644 --- a/.eslint-ignore +++ b/.eslint-ignore @@ -13,6 +13,8 @@ **/extensions/notebook-renderers/renderer-out/index.js **/extensions/simple-browser/media/index.js **/extensions/terminal-suggest/src/completions/upstream/** +**/extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts +**/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts **/extensions/terminal-suggest/third_party/** **/extensions/typescript-language-features/test-workspace/** **/extensions/typescript-language-features/extension.webpack.config.js diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index d5f166a4aae..321db470c55 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -118,6 +118,9 @@ jobs: - name: Run Valid Layers Checks run: npm run valid-layers-check + - name: Run Property Init Order Checks + run: npm run property-init-order-check + - name: Compile /build/ run: npm run compile working-directory: build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 344213e0ca8..813b8dac765 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -299,6 +299,9 @@ jobs: - name: Run Valid Layers Checks run: npm run valid-layers-check + - name: Run Property Init Order Checks + run: npm run property-init-order-check + - name: Compile /build/ run: npm run compile working-directory: build diff --git a/.github/workflows/telemetry.yml b/.github/workflows/telemetry.yml index d29ea6c58da..a5ac3be4198 100644 --- a/.github/workflows/telemetry.yml +++ b/.github/workflows/telemetry.yml @@ -14,6 +14,6 @@ jobs: node-version: 'lts/*' - name: 'Run vscode-telemetry-extractor' - run: 'npx --package=@vscode/telemetry-extractor --yes vscode-telemetry-extractor -s .' + run: 'npx --package=@vscode/telemetry-extractor@1.14.0 --yes vscode-telemetry-extractor -s .' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.npmrc b/.npmrc index bfa6fba99e3..71d98e6c583 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" -target="34.2.0" -ms_build_id="11044223" +target="34.3.2" +ms_build_id="11161073" runtime="electron" build_from_source="true" legacy-peer-deps="true" diff --git a/.vscode-test.js b/.vscode-test.js index 917413ede2a..823ef615f4f 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -19,7 +19,7 @@ const { defineConfig } = require('@vscode/test-cli'); * A list of extension folders who have opted into tests, or configuration objects. * Edit me to add more! * - * @type {Array & { label: string })>} + * @type {Array & { label: string }>} */ const extensions = [ { @@ -65,6 +65,20 @@ const extensions = [ { label: 'microsoft-authentication', mocha: { timeout: 60_000 } + }, + { + label: 'vscode-api-tests-folder', + extensionDevelopmentPath: `extensions/vscode-api-tests`, + workspaceFolder: `extensions/vscode-api-tests/testWorkspace`, + mocha: { timeout: 60_000 }, + files: 'extensions/vscode-api-tests/out/singlefolder-tests/**/*.test.js', + }, + { + label: 'vscode-api-tests-workspace', + extensionDevelopmentPath: `extensions/vscode-api-tests`, + workspaceFolder: `extensions/vscode-api-tests/testworkspace.code-workspace`, + mocha: { timeout: 60_000 }, + files: 'extensions/vscode-api-tests/out/workspace-tests/**/*.test.js', } ]; @@ -75,9 +89,12 @@ const defaultLaunchArgs = process.env.API_TESTS_EXTRA_ARGS?.split(' ') || [ const config = defineConfig(extensions.map(extension => { /** @type {import('@vscode/test-cli').TestConfiguration} */ - const config = typeof extension === 'object' - ? { files: `extensions/${extension.label}/out/**/*.test.js`, ...extension } - : { files: `extensions/${extension}/out/**/*.test.js`, label: extension }; + const config = { + platform: 'desktop', + files: `extensions/${extension.label}/out/**/*.test.js`, + extensionDevelopmentPath: `extensions/${extension.label}`, + ...extension, + }; config.mocha ??= {}; if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { diff --git a/.vscode/launch.json b/.vscode/launch.json index afd57934304..f922b5d9c80 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -251,7 +251,51 @@ "timeout": 0, "env": { "VSCODE_EXTHOST_WILL_SEND_SOCKET": null, - "VSCODE_SKIP_PRELAUNCH": "1" + "VSCODE_SKIP_PRELAUNCH": "1", + }, + "cleanUp": "wholeBrowser", + "runtimeArgs": [ + "--inspect-brk=5875", + "--no-cached-data", + "--crash-reporter-directory=${workspaceFolder}/.profile-oss/crashes", + // for general runtime freezes: https://github.com/microsoft/vscode/issues/127861#issuecomment-904144910 + "--disable-features=CalculateNativeWinOcclusion", + "--disable-extension=vscode.vscode-api-tests" + ], + "userDataDir": "${userHome}/.vscode-oss-dev", + "webRoot": "${workspaceFolder}", + "cascadeTerminateToConfigurations": [ + "Attach to Extension Host" + ], + "pauseForSourceMap": false, + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "browserLaunchLocation": "workspace", + "presentation": { + "hidden": true, + }, + }, + { + // To debug observables you also need the extension "ms-vscode.debug-value-editor" + "type": "chrome", + "request": "launch", + "name": "Launch VS Code Internal (Dev Debug)", + "windows": { + "runtimeExecutable": "${workspaceFolder}/scripts/code.bat" + }, + "osx": { + "runtimeExecutable": "${workspaceFolder}/scripts/code.sh" + }, + "linux": { + "runtimeExecutable": "${workspaceFolder}/scripts/code.sh" + }, + "port": 9222, + "timeout": 0, + "env": { + "VSCODE_EXTHOST_WILL_SEND_SOCKET": null, + "VSCODE_SKIP_PRELAUNCH": "1", + "VSCODE_DEV_DEBUG": "1", }, "cleanUp": "wholeBrowser", "runtimeArgs": [ @@ -568,6 +612,21 @@ "order": 1 } }, + { + "name": "VS Code (Debug Observables)", + "stopAll": true, + "configurations": [ + "Launch VS Code Internal (Dev Debug)", + "Attach to Main Process", + "Attach to Extension Host", + "Attach to Shared Process", + ], + "preLaunchTask": "Ensure Prelaunch Dependencies", + "presentation": { + "group": "0_vscode", + "order": 1 + } + }, { "name": "Search, Renderer, and Main processes", "configurations": [ diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 09d6480e102..3a0eaec922f 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"February 2025\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"March 2025\"" }, { "kind": 1, diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 9f92bf0b6a6..ca93d503338 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"February 2025\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\r\n\r\n$MILESTONE=milestone:\"February 2025\"" }, { "kind": 1, @@ -97,7 +97,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE is:issue is:closed reason:completed label:verification-needed -label:verified" + "value": "$REPOS $MILESTONE is:issue is:closed reason:completed label:verification-needed -label:verified -label:on-testplan" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 9d5c8ccdcc5..0fd05ece485 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -62,7 +62,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE $MINE is:issue is:closed reason:completed label:feature-request label:verification-needed -label:verified" + "value": "$REPOS $MILESTONE $MINE is:issue is:closed reason:completed label:feature-request label:verification-needed -label:verified -label:on-testplan" }, { "kind": 1, @@ -87,7 +87,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed -assignee:@me -label:verified -label:z-author-verified label:feature-request label:verification-needed -label:verification-steps-needed -label:unreleased" + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed reason:completed -assignee:@me -label:verified -label:z-author-verified label:feature-request label:verification-needed -label:verification-steps-needed -label:unreleased -label:on-testplan" }, { "kind": 1, diff --git a/.vscode/settings.json b/.vscode/settings.json index 59a78245c83..80c37853fe7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -49,6 +49,7 @@ "out-vscode-reh/**": true, "extensions/**/dist/**": true, "extensions/**/out/**": true, + "extensions/terminal-suggest/src/completions/upstream/**": true, "test/smoke/out/**": true, "test/automation/out/**": true, "test/integration/browser/out/**": true @@ -156,6 +157,7 @@ "application.experimental.rendererProfiling": true, "editor.experimental.asyncTokenization": true, "editor.experimental.asyncTokenizationVerification": true, + "terminal.integrated.suggest.enabled": true, "typescript.preferences.autoImportFileExcludePatterns": [ "@xterm/xterm", "@xterm/headless", diff --git a/build/.npmrc b/build/.npmrc index 1b073e71a83..551822f79cd 100644 --- a/build/.npmrc +++ b/build/.npmrc @@ -2,4 +2,5 @@ disturl="https://nodejs.org/dist" runtime="node" build_from_source="true" legacy-peer-deps="true" +force_process_config="true" timeout=180000 diff --git a/build/azure-pipelines/cli/cli-compile.yml b/build/azure-pipelines/cli/cli-compile.yml index 71cd3f71e78..a5d8bdc1a2c 100644 --- a/build/azure-pipelines/cli/cli-compile.yml +++ b/build/azure-pipelines/cli/cli-compile.yml @@ -42,7 +42,6 @@ steps: - script: | set -e if [ -n "$SYSROOT_ARCH" ]; then - export VSCODE_SYSROOT_PREFIX='-glibc-2.17' export VSCODE_SYSROOT_DIR=$(Build.SourcesDirectory)/.build/sysroots node -e '(async () => { const { getVSCodeSysroot } = require("../build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' if [ "$SYSROOT_ARCH" == "arm64" ]; then @@ -73,7 +72,7 @@ steps: # verify glibc requirement if [ -n "$SYSROOT_ARCH" ]; then - glibc_version="2.17" + glibc_version="2.28" while IFS= read -r line; do if [[ $line == *"GLIBC_"* ]]; then version=$(echo "$line" | awk '{print $5}' | tr -d '()') @@ -83,8 +82,8 @@ steps: fi fi done < <("$OBJDUMP" -T "$PWD/target/${{ parameters.VSCODE_CLI_TARGET }}/release/code") - if [[ "$glibc_version" != "2.17" ]]; then - echo "Error: binary has dependency on GLIBC > 2.17, found $glibc_version" + if [[ "$glibc_version" != "2.28" ]]; then + echo "Error: binary has dependency on GLIBC > 2.28, found $glibc_version" exit 1 fi fi @@ -120,22 +119,6 @@ steps: ArtifactServices.Symbol.UseAAD: false displayName: Publish Symbols - - task: CopyFiles@2 - inputs: - SourceFolder: $(Build.SourcesDirectory)/cli/target/${{ parameters.VSCODE_CLI_TARGET }}/release - Contents: 'code.*' - TargetFolder: $(Agent.TempDirectory)/binskim-cli - displayName: Copy files for BinSkim - - - task: BinSkim@4 - inputs: - InputType: Basic - Function: analyze - TargetPattern: guardianGlob - AnalyzeTargetGlob: $(Agent.TempDirectory)/binskim-cli/*.* - AnalyzeSymPath: $(Agent.TempDirectory)/binskim-cli - displayName: Run BinSkim - - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" diff --git a/build/azure-pipelines/cli/install-rust-posix.yml b/build/azure-pipelines/cli/install-rust-posix.yml index fee56e028f7..0607cde33e5 100644 --- a/build/azure-pipelines/cli/install-rust-posix.yml +++ b/build/azure-pipelines/cli/install-rust-posix.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.81 + default: 1.85 - name: targets default: [] type: object diff --git a/build/azure-pipelines/cli/install-rust-win32.yml b/build/azure-pipelines/cli/install-rust-win32.yml index 45a1cfd188e..bff114fccd0 100644 --- a/build/azure-pipelines/cli/install-rust-win32.yml +++ b/build/azure-pipelines/cli/install-rust-win32.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.81 + default: 1.85 - name: targets default: [] type: object diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index 444e005d3c6..8e052881b2c 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -382,7 +382,7 @@ async function unzip(packagePath, outputPath) { }); } // Contains all of the logic for mapping details to our actual product names in CosmosDB -function getPlatform(product, os, arch, type, isLegacy) { +function getPlatform(product, os, arch, type) { switch (os) { case 'win32': switch (product) { @@ -427,12 +427,12 @@ function getPlatform(product, os, arch, type, isLegacy) { case 'client': return `linux-${arch}`; case 'server': - return isLegacy ? `server-linux-legacy-${arch}` : `server-linux-${arch}`; + return `server-linux-${arch}`; case 'web': if (arch === 'standalone') { return 'web-standalone'; } - return isLegacy ? `server-linux-legacy-${arch}-web` : `server-linux-${arch}-web`; + return `server-linux-${arch}-web`; default: throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } @@ -556,8 +556,7 @@ async function processArtifact(artifact, filePath) { await releaseService.createRelease(version, filePath, friendlyFileName); } const { product, os, arch, unprocessedType } = match.groups; - const isLegacy = artifact.name.includes('_legacy'); - const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); + const platform = getPlatform(product, os, arch, unprocessedType); const type = getRealType(unprocessedType); const size = fs_1.default.statSync(filePath).size; const stream = fs_1.default.createReadStream(filePath); @@ -610,9 +609,6 @@ async function main() { if (e('VSCODE_BUILD_STAGE_LINUX') === 'True') { stages.add('Linux'); } - if (e('VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER') === 'True') { - stages.add('LinuxLegacyServer'); - } if (e('VSCODE_BUILD_STAGE_ALPINE') === 'True') { stages.add('Alpine'); } diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index f061d043d13..37bf5ccc6cd 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -694,7 +694,7 @@ interface Asset { } // Contains all of the logic for mapping details to our actual product names in CosmosDB -function getPlatform(product: string, os: string, arch: string, type: string, isLegacy: boolean): string { +function getPlatform(product: string, os: string, arch: string, type: string): string { switch (os) { case 'win32': switch (product) { @@ -739,12 +739,12 @@ function getPlatform(product: string, os: string, arch: string, type: string, is case 'client': return `linux-${arch}`; case 'server': - return isLegacy ? `server-linux-legacy-${arch}` : `server-linux-${arch}`; + return `server-linux-${arch}`; case 'web': if (arch === 'standalone') { return 'web-standalone'; } - return isLegacy ? `server-linux-legacy-${arch}-web` : `server-linux-${arch}-web`; + return `server-linux-${arch}-web`; default: throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } @@ -896,8 +896,7 @@ async function processArtifact( } const { product, os, arch, unprocessedType } = match.groups!; - const isLegacy = artifact.name.includes('_legacy'); - const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); + const platform = getPlatform(product, os, arch, unprocessedType); const type = getRealType(unprocessedType); const size = fs.statSync(filePath).size; const stream = fs.createReadStream(filePath); @@ -956,7 +955,6 @@ async function main() { if (e('VSCODE_BUILD_STAGE_WINDOWS') === 'True') { stages.add('Windows'); } if (e('VSCODE_BUILD_STAGE_LINUX') === 'True') { stages.add('Linux'); } - if (e('VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER') === 'True') { stages.add('LinuxLegacyServer'); } if (e('VSCODE_BUILD_STAGE_ALPINE') === 'True') { stages.add('Alpine'); } if (e('VSCODE_BUILD_STAGE_MACOS') === 'True') { stages.add('macOS'); } if (e('VSCODE_BUILD_STAGE_WEB') === 'True') { stages.add('Web'); } diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index e77000d431b..bd37d675aa2 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -121,6 +121,11 @@ steps: - template: ../common/install-builtin-extensions.yml@self + - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: + - script: node build/lib/policies darwin + displayName: Generate policy definitions + retryCountOnTaskFailure: 3 + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | set -e diff --git a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml deleted file mode 100644 index 4c26baf2f99..00000000000 --- a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml +++ /dev/null @@ -1,233 +0,0 @@ -parameters: - - name: VSCODE_QUALITY - type: string - - name: VSCODE_RUN_INTEGRATION_TESTS - type: boolean - - name: VSCODE_ARCH - type: string - -steps: - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - - template: ../distro/download-distro.yml - - - task: AzureKeyVault@2 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" - - - task: DownloadPipelineArtifact@2 - inputs: - artifact: Compilation - path: $(Build.ArtifactStagingDirectory) - displayName: Download compilation output - - - script: tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz - displayName: Extract compilation output - - - script: | - set -e - # Start X server - ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get update - ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y pkg-config \ - dbus \ - xvfb \ - libgtk-3-0 \ - libxkbfile-dev \ - libkrb5-dev \ - libgbm1 \ - rpm \ - gcc-10 \ - g++-10 - 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 - # Start dbus session - sudo mkdir -p /var/run/dbus - 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 - - - script: node build/setup-npm-registry.js $NPM_REGISTRY - condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Registry - - - script: | - set -e - # Set the private NPM registry to the global npmrc file - # so that authentication works for subfolders like build/, remote/, extensions/ etc - # which does not have their own .npmrc file - npm config set registry "$NPM_REGISTRY" - echo "##vso[task.setvariable variable=NPMRC_PATH]$(npm config get userconfig)" - condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM - - - task: npmAuthenticate@0 - inputs: - workingFile: $(NPMRC_PATH) - condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication - - - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: - - task: Docker@1 - displayName: "Pull Docker image" - inputs: - azureSubscriptionEndpoint: vscode - azureContainerRegistry: vscodehub.azurecr.io - command: "Run an image" - imageName: vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) - containerCommand: uname - - - script: | - set -e - - for i in {1..5}; do # try 5 times - npm ci && break - if [ $i -eq 5 ]; then - echo "Npm install failed too many times" >&2 - exit 1 - fi - echo "Npm install failed $i, trying again..." - done - workingDirectory: build - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Install build dependencies - - - script: | - set -e - - export VSCODE_SYSROOT_PREFIX='-glibc-2.17' - export CC=$(which gcc-10) - export CXX=$(which g++-10) - source ./build/azure-pipelines/linux/setup-env.sh --skip-sysroot - - for i in {1..5}; do # try 5 times - npm ci && break - if [ $i -eq 5 ]; then - echo "Npm install failed too many times" >&2 - exit 1 - fi - echo "Npm install failed $i, trying again..." - done - env: - npm_config_arch: $(NPM_ARCH) - VSCODE_ARCH: $(VSCODE_ARCH) - NPM_REGISTRY: "$(NPM_REGISTRY)" - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: - VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) - displayName: Install dependencies - - - script: node build/azure-pipelines/distro/mixin-npm - displayName: Mixin distro node modules - - - script: node build/azure-pipelines/distro/mixin-quality - displayName: Mixin distro quality - - - template: ../common/install-builtin-extensions.yml - - - script: | - set -e - npm run gulp vscode-linux-$(VSCODE_ARCH)-min-ci - ARCHIVE_PATH=".build/linux/client/code-${{ parameters.VSCODE_QUALITY }}-$(VSCODE_ARCH)-$(date +%s).tar.gz" - mkdir -p $(dirname $ARCHIVE_PATH) - echo "##vso[task.setvariable variable=CLIENT_PATH]$ARCHIVE_PATH" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build client - - - script: | - set -e - tar -czf $CLIENT_PATH -C .. VSCode-linux-$(VSCODE_ARCH) - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Archive client - - - script: | - set -e - export VSCODE_NODE_GLIBC="-glibc-2.17" - npm run gulp vscode-reh-linux-$(VSCODE_ARCH)-min-ci - mv ../vscode-reh-linux-$(VSCODE_ARCH) ../vscode-server-linux-$(VSCODE_ARCH) # TODO@joaomoreno - ARCHIVE_PATH=".build/linux/server/vscode-server-linux-legacy-$(VSCODE_ARCH).tar.gz" - UNARCHIVE_PATH="`pwd`/../vscode-server-linux-$(VSCODE_ARCH)" - mkdir -p $(dirname $ARCHIVE_PATH) - tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH) - echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" - echo "##vso[task.setvariable variable=SERVER_UNARCHIVE_PATH]$UNARCHIVE_PATH" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build server - - - script: | - set -e - export VSCODE_NODE_GLIBC="-glibc-2.17" - npm run gulp vscode-reh-web-linux-$(VSCODE_ARCH)-min-ci - mv ../vscode-reh-web-linux-$(VSCODE_ARCH) ../vscode-server-linux-$(VSCODE_ARCH)-web # TODO@joaomoreno - ARCHIVE_PATH=".build/linux/web/vscode-server-linux-legacy-$(VSCODE_ARCH)-web.tar.gz" - mkdir -p $(dirname $ARCHIVE_PATH) - tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH)-web - echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build server (web) - - - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: - - script: | - set -e - EXPECTED_GLIBC_VERSION="2.17" \ - EXPECTED_GLIBCXX_VERSION="3.4.19" \ - ./build/azure-pipelines/linux/verify-glibc-requirements.sh - env: - SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) - displayName: Check GLIBC and GLIBCXX dependencies in server archive - - - ${{ else }}: - - script: | - set -e - EXPECTED_GLIBC_VERSION="2.17" \ - EXPECTED_GLIBCXX_VERSION="3.4.22" \ - ./build/azure-pipelines/linux/verify-glibc-requirements.sh - env: - SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) - displayName: Check GLIBC and GLIBCXX dependencies in server archive - - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - template: product-build-linux-test.yml - parameters: - VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: ${{ parameters.VSCODE_RUN_INTEGRATION_TESTS }} - VSCODE_RUN_SMOKE_TESTS: false - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - PUBLISH_TASK_NAME: 1ES.PublishPipelineArtifact@1 - - - task: 1ES.PublishPipelineArtifact@1 - inputs: - targetPath: $(SERVER_PATH) - artifactName: $(ARTIFACT_PREFIX)vscode_server_linux_legacy_$(VSCODE_ARCH)_archive-unsigned - sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH) - sbomPackageName: "VS Code Linux $(VSCODE_ARCH) Legacy Server" - sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], '')) - displayName: Publish server archive - - - task: 1ES.PublishPipelineArtifact@1 - inputs: - targetPath: $(WEB_PATH) - artifactName: $(ARTIFACT_PREFIX)vscode_web_linux_legacy_$(VSCODE_ARCH)_archive-unsigned - sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH)-web - sbomPackageName: "VS Code Linux $(VSCODE_ARCH) Legacy Web" - sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], '')) - displayName: Publish web server archive diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index 1f198441bc3..94105642b19 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -16,56 +16,50 @@ else fi if [ "$npm_config_arch" == "x64" ]; then - # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/132.0.6834.196/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + # Download clang based on chromium revision used by vscode + curl -s https://raw.githubusercontent.com/chromium/chromium/132.0.6834.210/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux - # Download libcxx headers and objects from upstream electron releases - DEBUG=libcxx-fetcher \ - VSCODE_LIBCXX_OBJECTS_DIR=$PWD/.build/libcxx-objects \ - VSCODE_LIBCXX_HEADERS_DIR=$PWD/.build/libcxx_headers \ - VSCODE_LIBCXXABI_HEADERS_DIR=$PWD/.build/libcxxabi_headers \ - VSCODE_ARCH="$npm_config_arch" \ - node build/linux/libcxx-fetcher.js + # Download libcxx headers and objects from upstream electron releases + DEBUG=libcxx-fetcher \ + VSCODE_LIBCXX_OBJECTS_DIR=$PWD/.build/libcxx-objects \ + VSCODE_LIBCXX_HEADERS_DIR=$PWD/.build/libcxx_headers \ + VSCODE_LIBCXXABI_HEADERS_DIR=$PWD/.build/libcxxabi_headers \ + VSCODE_ARCH="$npm_config_arch" \ + node build/linux/libcxx-fetcher.js - # Set compiler toolchain - # Flags for the client build are based on - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.196:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.196:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.196:build/config/c++/BUILD.gn - export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" - export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" - export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -DSPDLOG_USE_STD_FORMAT -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" - export LDFLAGS="-stdlib=libc++ --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu -Wl,--lto-O0" + # Set compiler toolchain + # Flags for the client build are based on + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:build/config/c++/BUILD.gn + export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" + export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" + export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -DSPDLOG_USE_STD_FORMAT -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export LDFLAGS="-stdlib=libc++ --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu -Wl,--lto-O0" - if [ "$(echo "$@" | grep -c -- "--skip-sysroot")" -eq 0 ]; then - # Set compiler toolchain for remote server - export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc - export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-g++ - export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" - export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu" - fi + # Set compiler toolchain for remote server + export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc + export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu" elif [ "$npm_config_arch" == "arm64" ]; then - if [ "$(echo "$@" | grep -c -- "--skip-sysroot")" -eq 0 ]; then - # Set compiler toolchain for client native modules - export CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc - export CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ - export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" - export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" + # Set compiler toolchain for client native modules + export CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc + export CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ + export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" - # Set compiler toolchain for remote server - export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc - export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ - export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" - export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" - fi + # Set compiler toolchain for remote server + export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc + export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" elif [ "$npm_config_arch" == "arm" ]; then - if [ "$(echo "$@" | grep -c -- "--skip-sysroot")" -eq 0 ]; then - # Set compiler toolchain for client native modules - export CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc - export CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ - export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" - export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" - fi + # Set compiler toolchain for client native modules + export CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc + export CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ + export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" # Set compiler toolchain for remote server export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 7e680884b45..ff4f0db5679 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -41,26 +41,14 @@ parameters: displayName: "🎯 Linux x64" type: boolean default: true - - name: VSCODE_BUILD_LINUX_X64_LEGACY_SERVER - displayName: "🎯 Linux x64 Legacy Server" - type: boolean - default: true - name: VSCODE_BUILD_LINUX_ARM64 displayName: "🎯 Linux arm64" type: boolean default: true - - name: VSCODE_BUILD_LINUX_ARM64_LEGACY_SERVER - displayName: "🎯 Linux arm64 Legacy Server" - type: boolean - default: true - name: VSCODE_BUILD_LINUX_ARMHF displayName: "🎯 Linux armhf" type: boolean default: true - - name: VSCODE_BUILD_LINUX_ARMHF_LEGACY_SERVER - displayName: "🎯 Linux armhf Legacy Server" - type: boolean - default: true - name: VSCODE_BUILD_ALPINE displayName: "🎯 Alpine x64" type: boolean @@ -106,7 +94,10 @@ variables: - name: VSCODE_PRIVATE_BUILD value: ${{ ne(variables['Build.Repository.Uri'], 'https://github.com/microsoft/vscode.git') }} - name: NPM_REGISTRY - value: ${{ parameters.NPM_REGISTRY }} + ${{ if in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI') }}: # disable terrapin when in VSCODE_CIBUILD + value: none + ${{ else }}: + value: ${{ parameters.NPM_REGISTRY }} - name: CARGO_REGISTRY value: ${{ parameters.CARGO_REGISTRY }} - name: VSCODE_QUALITY @@ -115,8 +106,6 @@ variables: value: ${{ or(eq(parameters.VSCODE_BUILD_WIN32, true), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }} - name: VSCODE_BUILD_STAGE_LINUX value: ${{ or(eq(parameters.VSCODE_BUILD_LINUX, true), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true)) }} - - name: VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER - value: ${{ or(eq(parameters.VSCODE_BUILD_LINUX_X64_LEGACY_SERVER, true), eq(parameters.VSCODE_BUILD_LINUX_ARMHF_LEGACY_SERVER, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64_LEGACY_SERVER, true)) }} - name: VSCODE_BUILD_STAGE_ALPINE value: ${{ or(eq(parameters.VSCODE_BUILD_ALPINE, true), eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true)) }} - name: VSCODE_BUILD_STAGE_MACOS @@ -129,8 +118,6 @@ variables: value: ${{ and(eq(parameters.VSCODE_PUBLISH, true), eq(variables.VSCODE_CIBUILD, false), eq(parameters.VSCODE_COMPILE_ONLY, false)) }} - name: VSCODE_SCHEDULEDBUILD value: ${{ eq(variables['Build.Reason'], 'Schedule') }} - - name: VSCODE_7PM_BUILD - value: ${{ in(variables['Build.Reason'], 'BuildCompletion', 'ResourceTrigger') }} - name: VSCODE_STEP_ON_IT value: ${{ eq(parameters.VSCODE_STEP_ON_IT, true) }} - name: VSCODE_BUILD_MACOS_UNIVERSAL @@ -182,6 +169,8 @@ extends: tsa: enabled: true configFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/tsaoptions.json + binskim: + analyzeTargetGlob: '+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.exe;+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.node;+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.dll;-:file|$(Build.SourcesDirectory)/.build/**/system-setup/VSCodeSetup*.exe;-:file|$(Build.SourcesDirectory)/.build/**/user-setup/VSCodeUserSetup*.exe' codeql: runSourceLanguagesInSourceAnalysis: true compiled: @@ -278,6 +267,10 @@ extends: name: Azure Pipelines image: macOS-13 os: macOS + variables: + # todo@connor4312 to diagnose build flakes + - name: MSRUSTUP_LOG + value: debug steps: - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self parameters: @@ -291,6 +284,10 @@ extends: name: Azure Pipelines image: macOS-13 os: macOS + variables: + # todo@connor4312 to diagnose build flakes + - name: MSRUSTUP_LOG + value: debug steps: - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self parameters: @@ -321,13 +318,13 @@ extends: VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false)) }}: - - stage: CustomSDL + - stage: APIScan dependsOn: [] pool: name: 1es-windows-2019-x64 os: windows jobs: - - job: WindowsSDL + - job: WindowsAPIScan steps: - template: build/azure-pipelines/win32/sdl-scan-win32.yml@self parameters: @@ -546,51 +543,6 @@ extends: VSCODE_RUN_INTEGRATION_TESTS: false VSCODE_RUN_SMOKE_TESTS: false - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER'], true)) }}: - - stage: LinuxLegacyServer - dependsOn: - - Compile - pool: - name: 1es-ubuntu-20.04-x64 - os: linux - jobs: - - ${{ if eq(parameters.VSCODE_BUILD_LINUX_X64_LEGACY_SERVER, true) }}: - - job: Linuxx64LegacyServer - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: build/azure-pipelines/linux/product-build-linux-legacy-server.yml@self - parameters: - VSCODE_ARCH: x64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - - - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARMHF_LEGACY_SERVER, true) }}: - - job: LinuxArmhfLegacyServer - variables: - VSCODE_ARCH: armhf - NPM_ARCH: arm - steps: - - template: build/azure-pipelines/linux/product-build-linux-legacy-server.yml@self - parameters: - VSCODE_ARCH: armhf - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_INTEGRATION_TESTS: false - - - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARM64_LEGACY_SERVER, true) }}: - - job: LinuxArm64LegacyServer - variables: - VSCODE_ARCH: arm64 - NPM_ARCH: arm64 - steps: - - template: build/azure-pipelines/linux/product-build-linux-legacy-server.yml@self - parameters: - VSCODE_ARCH: arm64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_INTEGRATION_TESTS: false - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_ALPINE'], true)) }}: - stage: Alpine dependsOn: diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index dcc1a62225d..6096157ad8d 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -104,12 +104,12 @@ steps: - template: common/install-builtin-extensions.yml@self - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - - script: npm exec -- npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check + - script: npm exec -- npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check property-init-order-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile & Hygiene (OSS) - ${{ else }}: - - script: npm exec -- npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check + - script: npm exec -- npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check property-init-order-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile & Hygiene (non-OSS) diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index 8ecf5e6238e..1127e5212e9 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -102,7 +102,6 @@ steps: $stages = @( if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' } if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' } - if ($env:VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER -eq 'True') { 'LinuxLegacyServer' } if ($env:VSCODE_BUILD_STAGE_ALPINE -eq 'True') { 'Alpine' } if ($env:VSCODE_BUILD_STAGE_MACOS -eq 'True') { 'macOS' } if ($env:VSCODE_BUILD_STAGE_WEB -eq 'True') { 'Web' } diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index ed7f10048ca..6251417f193 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -124,7 +124,7 @@ steps: - template: ../common/install-builtin-extensions.yml@self - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: - - powershell: node build\lib\policies + - powershell: node build\lib\policies win32 displayName: Generate Group Policy definitions retryCountOnTaskFailure: 3 @@ -145,7 +145,7 @@ steps: exec { npm run gulp "vscode-win32-$(VSCODE_ARCH)-min-ci" } exec { npm run gulp "vscode-win32-$(VSCODE_ARCH)-inno-updater" } echo "##vso[task.setvariable variable=BUILT_CLIENT]true" - echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)" + echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build client @@ -156,7 +156,7 @@ steps: exec { npm run gulp "vscode-reh-win32-$(VSCODE_ARCH)-min-ci" } mv ..\vscode-reh-win32-$(VSCODE_ARCH) ..\vscode-server-win32-$(VSCODE_ARCH) # TODO@joaomoreno echo "##vso[task.setvariable variable=BUILT_SERVER]true" - echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(CodeSigningFolderPath),$(agent.builddirectory)/vscode-server-win32-$(VSCODE_ARCH)" + echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(CodeSigningFolderPath),$(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server @@ -196,10 +196,10 @@ steps: $ErrorActionPreference = "Stop" $ArtifactName = (gci -Path "$(Build.ArtifactStagingDirectory)/cli" | Select-Object -last 1).FullName Expand-Archive -Path $ArtifactName -DestinationPath "$(Build.ArtifactStagingDirectory)/cli" - $AppProductJson = Get-Content -Raw -Path "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)\resources\app\product.json" | ConvertFrom-Json + $AppProductJson = Get-Content -Raw -Path "$(Agent.BuildDirectory)\VSCode-win32-$(VSCODE_ARCH)\resources\app\product.json" | ConvertFrom-Json $CliAppName = $AppProductJson.tunnelApplicationName $AppName = $AppProductJson.applicationName - Move-Item -Path "$(Build.ArtifactStagingDirectory)/cli/$AppName.exe" -Destination "$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)/bin/$CliAppName.exe" + Move-Item -Path "$(Build.ArtifactStagingDirectory)/cli/$AppName.exe" -Destination "$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)/bin/$CliAppName.exe" displayName: Move VS Code CLI - task: UseDotNet@2 diff --git a/build/azure-pipelines/win32/sdl-scan-win32.yml b/build/azure-pipelines/win32/sdl-scan-win32.yml index def3cb53dfc..bf6819a4b47 100644 --- a/build/azure-pipelines/win32/sdl-scan-win32.yml +++ b/build/azure-pipelines/win32/sdl-scan-win32.yml @@ -102,13 +102,6 @@ steps: - powershell: npm run compile displayName: Compile - - powershell: | - Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.exe" - Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.dll" - Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.node" - Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.pdb" - displayName: List files - - powershell: npm run gulp "vscode-symbols-win32-${{ parameters.VSCODE_ARCH }}" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" @@ -119,16 +112,7 @@ steps: Get-ChildItem '$(Agent.BuildDirectory)\scanbin' -Recurse -Filter "*.dll" Get-ChildItem '$(Agent.BuildDirectory)\scanbin' -Recurse -Filter "*.node" Get-ChildItem '$(Agent.BuildDirectory)\scanbin' -Recurse -Filter "*.pdb" - displayName: List files again - - - task: BinSkim@4 - inputs: - InputType: "Basic" - Function: "analyze" - TargetPattern: "guardianGlob" - AnalyzeIgnorePdbLoadError: true - AnalyzeTargetGlob: '$(Agent.BuildDirectory)\scanbin\**.dll;$(Agent.BuildDirectory)\scanbin\**.exe;$(Agent.BuildDirectory)\scanbin\**.node' - AnalyzeLocalSymbolDirectories: '$(Agent.BuildDirectory)\scanbin\VSCode-win32-${{ parameters.VSCODE_ARCH }}\pdb' + displayName: List files - task: CopyFiles@2 displayName: 'Collect Symbols for API Scan' @@ -139,19 +123,6 @@ steps: flattenFolders: true condition: succeeded() - - task: PublishSymbols@2 - inputs: - IndexSources: false - SymbolsFolder: '$(Agent.BuildDirectory)\symbols' - SearchPattern: '**\*.pdb' - SymbolServerType: TeamServices - SymbolsProduct: 'code' - ArtifactServices.Symbol.AccountName: microsoft - ArtifactServices.Symbol.PAT: $(System.AccessToken) - ArtifactServices.Symbol.UseAAD: false - displayName: Publish Symbols - condition: succeeded() - - task: APIScan@2 inputs: softwareFolder: $(Agent.BuildDirectory)\scanbin diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index dcc7bf55c5c..d390d7a233c 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -c0a187acca68906c4a6387e8fabd052cb031ace6132d60a71001d9a0e891958e *chromedriver-v34.2.0-darwin-arm64.zip -fa5a46d752267d8497d375e19079e8b6a8df70c234a79b2d6b48f5862e1a0abc *chromedriver-v34.2.0-darwin-x64.zip -61e03d4fa570976d80f740637f56192b6448a05a73d1fba9717900b29f2b1b4d *chromedriver-v34.2.0-linux-arm64.zip -ec774d9b1a1b828a0db1502a1017fcab1dfed99b1b6b2fd2308dd600a1efa98a *chromedriver-v34.2.0-linux-armv7l.zip -cc15a6e6206485a2d96649ceb60509b9da04fa2811c4824b2e0eb43d1f4b1417 *chromedriver-v34.2.0-linux-x64.zip -9777122f6684180ef375b9b21dcabbc731d8a8befa300d1d47ad954a5b64c1c8 *chromedriver-v34.2.0-mas-arm64.zip -69451fa148b105fec9644646b22ca758a206499574c5816591354835c8056679 *chromedriver-v34.2.0-mas-x64.zip -eb7adc7e720f5e0f1d2c12ecbe886bdc01f2c9aaa3954bd6ebd313750bb18819 *chromedriver-v34.2.0-win32-arm64.zip -cb1973b0c2f5565974d5c2cb51816692f064b6cdc7897fa341d528ba7f9b14bf *chromedriver-v34.2.0-win32-ia32.zip -af5575b4727c3dbe7272016cbbaa8872043f843168a47d86748a50397efb4f77 *chromedriver-v34.2.0-win32-x64.zip -fcc718af2a28fb953290dc971e945818b4dbb293f297e6e25acb669d450cc0dd *electron-api.json -fa47e752e559a6472b87d8907f63296ed8cd53ecf862a4ae113018474d40aa8e *electron-v34.2.0-darwin-arm64-dsym-snapshot.zip -336c3374e721e2379901141be6345459f78d243b037c65b55bac4ae8cb14bfbe *electron-v34.2.0-darwin-arm64-dsym.zip -ac3b9d712d9f036f066d8eba42797117a513e2d250fcc117f0354300b7d5c1e5 *electron-v34.2.0-darwin-arm64-symbols.zip -ee447c17b2ac545e48083113d7e39a916821c1316f60f42cbcbee4fffe7c022a *electron-v34.2.0-darwin-arm64.zip -d7510bc038d06b26690ac9a78fbb256626502303ff7f5b1c2242f64a02832b1b *electron-v34.2.0-darwin-x64-dsym-snapshot.zip -0e95c2bbda00afe78e6229b824e3ffe0cc8612956dc11a5a30380224acdbecae *electron-v34.2.0-darwin-x64-dsym.zip -6d734ef8e8fd007071aae9a13534894dd801f11900f3e73e49cf5352b426e575 *electron-v34.2.0-darwin-x64-symbols.zip -8ef741819c8a5370dabc3b9df5e6ac217366477c5d5c656ed23c800bc984d887 *electron-v34.2.0-darwin-x64.zip -c2f448882a0392ebfd9810058a6a9580b087002c74fca3dcd3cf4ba5c3b27a7c *electron-v34.2.0-linux-arm64-debug.zip -51e887c382593021127593ceba89ad662d3a6de0f748b94fe3a0ce78a7393923 *electron-v34.2.0-linux-arm64-symbols.zip -818c91309da8ff948c43df58a996c05c0d27daa690e1d659355d9db01e351919 *electron-v34.2.0-linux-arm64.zip -c2f448882a0392ebfd9810058a6a9580b087002c74fca3dcd3cf4ba5c3b27a7c *electron-v34.2.0-linux-armv7l-debug.zip -71ec2b7473db766194bcf04648229c4affedce06e150e692745cc72dbc3749c5 *electron-v34.2.0-linux-armv7l-symbols.zip -0c75996c6bf2d37d0441d3e1021386a9348f8d21783d75571cdb10ede606424f *electron-v34.2.0-linux-armv7l.zip -d1f17be8df8ec6dc1a23afd8a7752f0b949c755a493b8a2c1e83f76c629258ca *electron-v34.2.0-linux-x64-debug.zip -d8fa79154b0b663dbd0054d69f53aec326997449ef21943d27225a2d6899660f *electron-v34.2.0-linux-x64-symbols.zip -f12a02d86cc657500978d263ec6d1841b6e2085cd3bd4d30a97cfe14685396f3 *electron-v34.2.0-linux-x64.zip -71ef8bfebb8513a405fd2beb44ad18f802bbac2248f81a5328ddaaa12906d70c *electron-v34.2.0-mas-arm64-dsym-snapshot.zip -437aca6cad3158f15fd852e5913462637052940f812b20ccc10fccc133d5a0d6 *electron-v34.2.0-mas-arm64-dsym.zip -7f253375a7b43d34b770c03153e47185be7a64bc0c10a783993a93144eb35492 *electron-v34.2.0-mas-arm64-symbols.zip -1f601c20430b036b485c7dc02a39a297f0d08905462a4568306c12f299375e2f *electron-v34.2.0-mas-arm64.zip -d32664181804a17f825bf1fbfd8f0cbe01e0b41b284937359e6d9754b3481ed8 *electron-v34.2.0-mas-x64-dsym-snapshot.zip -f6b394b89fb77dfeefdf525739fe8cd9695f8c2ac2251ed7c571a376cde059e9 *electron-v34.2.0-mas-x64-dsym.zip -539328d93e9bc122e6e34faeab6a42f4845c523f796c7a11bbdcfe5e856f6b98 *electron-v34.2.0-mas-x64-symbols.zip -749b6ced7a9d35a719ad98d4c3bf1a08c315c19ce97d2497235d4ac302cda665 *electron-v34.2.0-mas-x64.zip -e843ea4cb7a93686728d056c957c124760886167932ff619b518e45917edf38a *electron-v34.2.0-win32-arm64-pdb.zip -4908423be5f8ad1b5dd737edfd69ee0870460e16bb639bb963b0981bda2628e4 *electron-v34.2.0-win32-arm64-symbols.zip -9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-arm64-toolchain-profile.zip -1629cec7b5620e6ca3b5305c393ae147d1a3871a8f164d686b7bee3810fc1109 *electron-v34.2.0-win32-arm64.zip -e7182f1ef5ed13187fe12f35133cefb50c59e1d52ada758cbb72813dda575205 *electron-v34.2.0-win32-ia32-pdb.zip -a10f778f62bf060a7e38f5ea75ea472679b9ef4f12767ee0703e6d77effae78a *electron-v34.2.0-win32-ia32-symbols.zip -9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-ia32-toolchain-profile.zip -96396712a0240f04471f71246b3885a513b08e3f2d40154272d34e59419db7e6 *electron-v34.2.0-win32-ia32.zip -0c55ef5b1a6ea4604e3f0e9f8b5e946944abd1cfc3b974432d40e924a9996812 *electron-v34.2.0-win32-x64-pdb.zip -d6b67cf12edabcc62ae21e7c90ac6b1161dbefe4e6b765c69fc7040096b7d026 *electron-v34.2.0-win32-x64-symbols.zip -9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-x64-toolchain-profile.zip -a4fdf617dca787b7f1c6f8d0d1cb69c8adb37ee23f9553fe69803f9cad713360 *electron-v34.2.0-win32-x64.zip -5ca44a4ee9cc74a56c9556ce3f3774986dbb8b4f4953088c7f4577bfba4356a8 *electron.d.ts -86247a6815cc98f321374edcbfc0ab7ee79bcb13a5c25213f42151e4059ec690 *ffmpeg-v34.2.0-darwin-arm64.zip -e94f4707a91194f97d0451f9d5a27982c873a5c13d83d20926ce6fdb613f536c *ffmpeg-v34.2.0-darwin-x64.zip -947d7b7cb035eab94cc15b602dfa8a925cf238c1d9225c01732fe0c41f59c571 *ffmpeg-v34.2.0-linux-arm64.zip -64d74b6b94ac4fd755b97e0ec8d50af9cd73810fdb52f6c081b13a3337ace0ea *ffmpeg-v34.2.0-linux-armv7l.zip -a62cb20c5e5f2ba6f1df6f1bc406cc30f7ed44fe6380b506afa333d1b4ad7a87 *ffmpeg-v34.2.0-linux-x64.zip -715bbb3d193b1ca57d4b329edd05cd6b125dc188cdb73d1c66f220f62c8562e3 *ffmpeg-v34.2.0-mas-arm64.zip -866bf47106e8c3e50f046783eb8f5c756069c6de962a46f4edc7a5ee7e4593ee *ffmpeg-v34.2.0-mas-x64.zip -71a2a6c4cca15ccbcbf8912f5d73f855b0ca79804f941f68527ae808f8163957 *ffmpeg-v34.2.0-win32-arm64.zip -399f841cca166781078ca42c8ea060e3d5850ec6cb79716186d0806f3ce20842 *ffmpeg-v34.2.0-win32-ia32.zip -afad2a44f20a0c0c01fb1fea637f3f821842399593c9961c74d650ca12d32cbb *ffmpeg-v34.2.0-win32-x64.zip -c95fdc9dba05aa68aeccb69d4c34f0cb1fb98d7f5291d974d0b638488693655f *hunspell_dictionaries.zip -0abe74138afdb6e45a085d77407659f13c75ab96f694313d4e98bd662f9c6df2 *libcxx-objects-v34.2.0-linux-arm64.zip -3d0cbf6fb150b006428eab78235856d2204d5e93ca85f162e429b4c8bd9b0d3b *libcxx-objects-v34.2.0-linux-armv7l.zip -3a5491e32cec825499919be1b8bf0550d28fe5a31ff00a95572d49a58bb4820a *libcxx-objects-v34.2.0-linux-x64.zip -ff8753d52f759041b8e5462125ee2b96798fe8b5047f8fb8ae60cd07af2fb13d *libcxx_headers.zip -c98cce0091681bc367a48f66c5f4602961aa9cb6dd1a995d8969d6b39ce732f3 *libcxxabi_headers.zip -78a9606190fb227460ddcd276ad540873595d6a82fd1007f42900f53347e3eb1 *mksnapshot-v34.2.0-darwin-arm64.zip -6d4587a36f509356a908b6752de527cfe8a294a2d435b82ea3a98288c3a3a178 *mksnapshot-v34.2.0-darwin-x64.zip -17737bd34f7feefc5143b745f2c4f0e5a41678780e144eb43de41f8d4b9d7852 *mksnapshot-v34.2.0-linux-arm64-x64.zip -565aa9a84d913b7b5eb8d3b868cff151cb8a6c16548233ffd92b2f9bf3789227 *mksnapshot-v34.2.0-linux-armv7l-x64.zip -7332d7864ab4e5ee7fa8d00580d9357f30d0e69d3d1434d67c61f79f666314a4 *mksnapshot-v34.2.0-linux-x64.zip -cc81bfc9894378a9fc8a777429e04cc80860663b3dff8eba1f8cc72559a4f23d *mksnapshot-v34.2.0-mas-arm64.zip -c98230088698638159f6e7e0c5ddde3cb4dba9fa81d76e3c58fc86f96b63ef81 *mksnapshot-v34.2.0-mas-x64.zip -cc7b42943d998e1083ad8119dc2201dd27d709c15aa2a9b78f409485d2927592 *mksnapshot-v34.2.0-win32-arm64-x64.zip -ade9e3126f113318e226f9bdeba068b4383b30a98517a361386eff448459effa *mksnapshot-v34.2.0-win32-ia32.zip -344292ea318dc0e21f37bc7c82d57a57449f0fb278d9868f1b1b597bdcb1f36f *mksnapshot-v34.2.0-win32-x64.zip +c9b82c9f381742e839fea00aeb14f24519bcaf38a0f4eed25532191701f9535b *chromedriver-v34.3.2-darwin-arm64.zip +d556c1e2b06f1bf131e83c2fb981de755c28e1083a884d257eb964815be16b0c *chromedriver-v34.3.2-darwin-x64.zip +1cabad4f3303ac2ff172a9f22185f64944dbaa6fc68271609077158eaefdee35 *chromedriver-v34.3.2-linux-arm64.zip +4213ce52c72ef414179b5c5c22ae8423847ff030d438296bd6c2aac763930a7b *chromedriver-v34.3.2-linux-armv7l.zip +3c64c08221fdfc0f4be60ea8b1b126f2ecca45f60001b63778522f711022c6ea *chromedriver-v34.3.2-linux-x64.zip +e8388734d88e011cb6cd79795431de9206820749219d80565ee49d90501d2bf3 *chromedriver-v34.3.2-mas-arm64.zip +3ad1dd37bd6e0bb37e8503898db7aedd56bd5213e6d6760b05c3d11f4625062b *chromedriver-v34.3.2-mas-x64.zip +d567b481a0f5d88e84bba7718f89fb08f56363bfc4cb5914e1c2086358a5c252 *chromedriver-v34.3.2-win32-arm64.zip +df6732e9dc61cb20a3c0b2a2de453aac7e2bd54e7cbff43512afa614852c15fa *chromedriver-v34.3.2-win32-ia32.zip +dda0765c8d064924632e18cd152014ecd767f3808fc51c8249a053bfb7ca70a2 *chromedriver-v34.3.2-win32-x64.zip +1945f15caff98f2e0f1ee539c483d352fb8d4d0c13f342caa7abe247676d828c *electron-api.json +c078bbf727b3c3026f60e07a0f4643b85c06c581b54be017d0a6c284ba6772d3 *electron-v34.3.2-darwin-arm64-dsym-snapshot.zip +35f587754d6a3272606258386bf73688d63dd53c7e572d3a7cbaae6f3f60bdae *electron-v34.3.2-darwin-arm64-dsym.zip +08b14ee02c98353de3c738120dfd017322666e82b914a7f6de9b9888dcc5c0f0 *electron-v34.3.2-darwin-arm64-symbols.zip +2a4aa7e8fa30f229e465ebd18d3e4722e2b41529dc51a68a954d333a7e556ffe *electron-v34.3.2-darwin-arm64.zip +1509ccdeb80024f5e3edd5ecf804b4cef4e47ea2bd74e33ef0b39044b0ccf892 *electron-v34.3.2-darwin-x64-dsym-snapshot.zip +3bbe5d587c3f582ed8c126b0fb635cc02ad9a14d077b04892fe6f862092445b0 *electron-v34.3.2-darwin-x64-dsym.zip +fa7ece82e6ecaf1c94ed341e8ebff98e64687c68fe113f52cd9a21400302e22f *electron-v34.3.2-darwin-x64-symbols.zip +23938c62257a65a863ed7aa7c7966ba5f257a7d3dc16c78293e826962cc39c5c *electron-v34.3.2-darwin-x64.zip +0547eecf8ab538d74fa854f591ce8b888a3dbb339256d2db3038e7bb2c6dd929 *electron-v34.3.2-linux-arm64-debug.zip +676d0dc2b1c1c85c8b2abbb8cd5376ee22ecdb910493b910d9ae5a998532136a *electron-v34.3.2-linux-arm64-symbols.zip +774e4ccb39d553e5487994a9f8c60774a90f08cdb049ff65f3963fc27c969ff2 *electron-v34.3.2-linux-arm64.zip +0547eecf8ab538d74fa854f591ce8b888a3dbb339256d2db3038e7bb2c6dd929 *electron-v34.3.2-linux-armv7l-debug.zip +ba33bf53fcb35dea568a2795f5b23ecf46c218abe8258946611c72a1f42f716c *electron-v34.3.2-linux-armv7l-symbols.zip +73ae92c8fffb351d3a455569cf57ce9a3f676f42bf61939c613c607fe0fc3bfb *electron-v34.3.2-linux-armv7l.zip +e61a9a69dd7ea6f2687708a8e83516670cdea53c728226e598e2f6f1fad5b77b *electron-v34.3.2-linux-x64-debug.zip +f1a04df7fe67dd1cd29e7b87871525458d2eb24c0cf3b5835a1c56974707562a *electron-v34.3.2-linux-x64-symbols.zip +7b74c0c4fae82e27c7e9cbca13e9763e046113dba8737d3e27de9a0b300ac87e *electron-v34.3.2-linux-x64.zip +8571a6aa83e00925ceb39fdc5a45a9f6b9aa3d92fd84951c6f252ed723aea4ae *electron-v34.3.2-mas-arm64-dsym-snapshot.zip +477410c6f9a6c5eeaedf376058a02c2996fc0a334aa40eeec7d3734c09347f4d *electron-v34.3.2-mas-arm64-dsym.zip +c2e62dcd6630cb51b2d8e2869e74e47d29bda785521cea6e82e546d0fc58aabb *electron-v34.3.2-mas-arm64-symbols.zip +a1698e8546a062fd59b7f8e5507a7f3220fb00b347f2377de83fc9a07f7f3507 *electron-v34.3.2-mas-arm64.zip +741a24ac230a3651dca81d211f9f00b835c428a5ed0c5f67d370d4e88b62f8d6 *electron-v34.3.2-mas-x64-dsym-snapshot.zip +aeff97ec9e5c9e173ac89e38acd94476025c5640d5f27be1e8c2abd54398bab3 *electron-v34.3.2-mas-x64-dsym.zip +9f14b66b1d612ac66697288e8763171c388f7f200854871a5f0ab464a6a921c2 *electron-v34.3.2-mas-x64-symbols.zip +c979d7e7175f1e8e03ca187997d4c156b878189fc3611b347fadebcb16f3e027 *electron-v34.3.2-mas-x64.zip +f43c700641e8220205dd356952e32718d113cf530520c4ed7209b59851eac266 *electron-v34.3.2-win32-arm64-pdb.zip +3ba6e01c99bffac6b5dd3fd6f122ecdb571cf6f675dc5498c65050bd7a382ef8 *electron-v34.3.2-win32-arm64-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-arm64-toolchain-profile.zip +9b9cb65d75a16782088b492f9ef3bb4d27525012b819c12bf29bd27e159d749b *electron-v34.3.2-win32-arm64.zip +1006e7af4c149114b5ebc3497617aaa6cd1bb0b131e0a225fd73709ff308f9c5 *electron-v34.3.2-win32-ia32-pdb.zip +1ecb6430cd04454f08f557c9579163f3552144bfcc0b67b768dad8868b5b891d *electron-v34.3.2-win32-ia32-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-ia32-toolchain-profile.zip +d004fd5f853754001fafaec33e383d1950b30c935ee71b297ec1c9e084355e9b *electron-v34.3.2-win32-ia32.zip +4e0721552fd2f09e9466e88089af8b965f1bfbc4ae00a59aaf6245b1d1efabfd *electron-v34.3.2-win32-x64-pdb.zip +9dea812a7e7cd0fb18e5fed9a99db5531959a068c24d3c0ecedceb644cd3ffa0 *electron-v34.3.2-win32-x64-symbols.zip +c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-x64-toolchain-profile.zip +1785e161420fb90d2331c26e50bba3413cae9625b7db3c8524ea02ade631efba *electron-v34.3.2-win32-x64.zip +722b304c31ddac58b0083d94a241c5276464f04bd8ea4fcbfd33051d197be103 *electron.d.ts +31ce159b2e47d1de5bc907d8e1c89726b0f2ba530ec2e9d7a8e5c723b1ccf6e0 *ffmpeg-v34.3.2-darwin-arm64.zip +565539bac64a6ee9cf6f188070f520210a1507341718f5dc388ac7c454b1e1d5 *ffmpeg-v34.3.2-darwin-x64.zip +6006ea0f46ab229feb2685be086b0fafd65981e2939dd2218a078459c75ab527 *ffmpeg-v34.3.2-linux-arm64.zip +9404ce2e85df7c40f809f2cf62c7af607de299839fe6b7ae978c3015500abcc8 *ffmpeg-v34.3.2-linux-armv7l.zip +79aec96898b7e2462826780ee0b52b9ab299dc662af333e128a34fd5ddae87f1 *ffmpeg-v34.3.2-linux-x64.zip +9190743c78210574faf5d5ecb82a00f8fa15e5f2253378cb925a99ca9d39961b *ffmpeg-v34.3.2-mas-arm64.zip +48915adcb1a6342efeda896035101300f0432c0304cfb38f2378e98c6309ebae *ffmpeg-v34.3.2-mas-x64.zip +745d5ef786de6d4a720475079836e2fda7b501cfcd255819485a47de5b24b74e *ffmpeg-v34.3.2-win32-arm64.zip +d0d86d60978439dc8ae4a723d4e4c1f853891d596bfd84033440a232fa762e2f *ffmpeg-v34.3.2-win32-ia32.zip +4441539fd8c9cbe79880ff1bade9bdc0c3744c33d7409130af6404e57ee401ff *ffmpeg-v34.3.2-win32-x64.zip +39edd1eeefe881aa75af0e438204e0b1c6e6724e34fa5819109276331c0c2c9a *hunspell_dictionaries.zip +20dd417536e5f4ebc01f480221284c0673729c27b082bc04e2922f16cd571719 *libcxx-objects-v34.3.2-linux-arm64.zip +7e53c5779c04f895f8282c0450ec4a63074d15a0e910e41006cfea133d0288af *libcxx-objects-v34.3.2-linux-armv7l.zip +92e2283c924ab523ffec3ea22513beaab6417f7fc3d570f57d51a1e1ceb7f510 *libcxx-objects-v34.3.2-linux-x64.zip +9bf3c6e8ad68f523fe086fada4148dd04e5bb0b9290d104873e66f2584a5cf50 *libcxx_headers.zip +34e4b44f9c5e08b557a2caed55456ce7690abab910196a783a2a47b58d2b9ac9 *libcxxabi_headers.zip +11f67635e6659f9188198e4086c51b89890b61a22f6c17c99eff35595ee8f51d *mksnapshot-v34.3.2-darwin-arm64.zip +c0add9ef4ac27c73fa043d04b4c9635fd3fd9f5c16d7a03e961864ba05252813 *mksnapshot-v34.3.2-darwin-x64.zip +6262adf86a340d8d28059937b01ef1117b93212e945fddbceea5c18d7e7c64f0 *mksnapshot-v34.3.2-linux-arm64-x64.zip +f7db8ebe91a1cc8d24ef6aad12949a18d8e4975ac296e3e5e9ecd88c9bccb143 *mksnapshot-v34.3.2-linux-armv7l-x64.zip +6642038e86bda362980ff1c8973a404e2b02efdd87de9e35b650fc1e743833da *mksnapshot-v34.3.2-linux-x64.zip +15883bf8e8cd737c3682d1e719d7cbac92f50b525681aac324dca876861dfc7d *mksnapshot-v34.3.2-mas-arm64.zip +4da23a950bfcc377ef21c37d496017ab4c36da03f3b41049ac114042c42608ce *mksnapshot-v34.3.2-mas-x64.zip +fab59573d3c2f9bdf31146a1896d24ac0c51f736aad86d2f3c7ecef13c05a7fd *mksnapshot-v34.3.2-win32-arm64-x64.zip +66f25e07c6f8d5d2009577a129440255a3baf63c929a5b60b2e77cd52e46105b *mksnapshot-v34.3.2-win32-ia32.zip +8168bfbf61882cfac80aed1e71e364e1c7f2fccd11eac298e6abade8b46894ea *mksnapshot-v34.3.2-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index d00d52bc25f..d394605dda3 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,7 +1,7 @@ -fa76d5b5340f14070ebaa88ef8faa28c1e9271502725e830cb52f0cf5b6493de node-v20.18.2-darwin-arm64.tar.gz -00a16bb0a82a2ad5d00d66b466ae1afa678482283747c27e9bce96668f334744 node-v20.18.2-darwin-x64.tar.gz -319789e8a055ff80793a05e633c8c5c9226050144a09da3747225b4ec56a2a99 node-v20.18.2-linux-arm64.tar.gz -65397a4a63960bda94718099698d2961623e9ef400f60f4c3a71add2268bccfb node-v20.18.2-linux-armv7l.tar.gz -eb5b031bdd728871c3b9a82655dbfa533bc262c0b6da1d09a86842430cef07d4 node-v20.18.2-linux-x64.tar.gz -83e7ad1b8c4d4d9c5e06849c3e8f3a5948a5eb6aa34c5bd973ba700e0386f42c win-arm64/node.exe -8487a277e92282904dfe0f860dbd5d229543e97a858a223fbe9c9b8670bbe170 win-x64/node.exe +1f15b7ed18a580af31cf32bc126572292d820f547bf55bf9cdce08041a24e1d9 node-v20.18.3-darwin-arm64.tar.gz +ba668f64df9239843fefcef095ee539f5ac5aa1b0fc15a71f1ecca16abedec7a node-v20.18.3-darwin-x64.tar.gz +93a9df19238adfaa289f4784041d03edaf2fdd89fbb247faffca2fe4a1000703 node-v20.18.3-linux-arm64.tar.gz +8a84eb34287db6a273066934d7195e429f57b91686b62fc19497210204a2b3de node-v20.18.3-linux-armv7l.tar.gz +9fc3952da39b20d1fcfdb777b198cc035485afbbb1004b4df93f35245d61151e node-v20.18.3-linux-x64.tar.gz +4258e333f4b95060681d61bffa762542a8068547d3dffebe57c575b38d380dda win-arm64/node.exe +528a9aa64888a2a3ba71c6aea89434dd5ab5cb3caa9f0f31345cf5facf685ab0 win-x64/node.exe diff --git a/build/checksums/vscode-sysroot.txt b/build/checksums/vscode-sysroot.txt index 0b5f38c60ce..67182b078ed 100644 --- a/build/checksums/vscode-sysroot.txt +++ b/build/checksums/vscode-sysroot.txt @@ -1,6 +1,3 @@ -68a17006021975ff271a1dd615f9db9eda7c25f2cc65e750c87980dc57a06c94 aarch64-linux-gnu-glibc-2.17.tar.gz 0de422a81683cf9e8cf875dbd1e0c27545ac3c775b2d53015daf3ca2b31d3f15 aarch64-linux-gnu-glibc-2.28.tar.gz -3ced48cb479f2cdba95aa649710fcb7778685551c745bbd76ac706c3c0ead9fb arm-rpi-linux-gnueabihf-glibc-2.17.tar.gz 7aea163f7fad8cc50000c86b5108be880121d35e2f55d016ef8c96bbe54129eb arm-rpi-linux-gnueabihf-glibc-2.28.tar.gz -5aae21115f1d284c3cdf32c83db15771b59bc80793f1423032abf5a823c0d658 x86_64-linux-gnu-glibc-2.17.tar.gz dbb927408393041664a020661f2641c9785741be3d29b050b9dac58980967784 x86_64-linux-gnu-glibc-2.28.tar.gz diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index 535d46eb174..7d3f9164d5f 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -27,6 +27,7 @@ async function main(buildDir) { const filesToSkip = [ '**/CodeResources', '**/Credits.rtf', + '**/policies/{*.mobileconfig,**/*.plist}', // TODO: Should we consider expanding this to other files in this area? '**/node_modules/@parcel/node-addon-api/nothing.target.mk' ]; diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index 9e013cdb10c..7872eccc63e 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -28,6 +28,7 @@ async function main(buildDir?: string) { const filesToSkip = [ '**/CodeResources', '**/Credits.rtf', + '**/policies/{*.mobileconfig,**/*.plist}', // TODO: Should we consider expanding this to other files in this area? '**/node_modules/@parcel/node-addon-api/nothing.target.mk' ]; diff --git a/build/filters.js b/build/filters.js index 1d18e0c942a..ece41209baf 100644 --- a/build/filters.js +++ b/build/filters.js @@ -57,6 +57,7 @@ module.exports.unicodeFilter = [ '!extensions/**/out/**', '!extensions/**/snippets/**', '!extensions/**/colorize-fixtures/**', + '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', '!src/vs/base/browser/dompurify/**', '!src/vs/workbench/services/keybinding/browser/keyboardLayouts/**', @@ -89,7 +90,8 @@ module.exports.indentationFilter = [ '!test/automation/out/**', '!test/monaco/out/**', '!test/smoke/out/**', - '!extensions/terminal-suggest/src/shell/zshBuiltinsCache.json', + '!extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts', + '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', '!extensions/terminal-suggest/src/completions/upstream/**', '!extensions/typescript-language-features/test-workspace/**', '!extensions/typescript-language-features/resources/walkthroughs/**', @@ -194,6 +196,8 @@ module.exports.tsFormattingFilter = [ '!extensions/vscode-api-tests/testWorkspace2/**', '!extensions/**/*.test.ts', '!extensions/html-language-features/server/lib/jquery.d.ts', + '!extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts', + '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', ]; module.exports.eslintFilter = [ diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index e0df76f1c8f..44add792d14 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -402,13 +402,7 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa ); } - if (platform === 'linux' && process.env['VSCODE_NODE_GLIBC'] === '-glibc-2.17') { - result = es.merge(result, - gulp.src(`resources/server/bin/helpers/check-requirements-linux-legacy.sh`, { base: '.' }) - .pipe(rename(`bin/helpers/check-requirements.sh`)) - .pipe(util.setExecutableBit()) - ); - } else if (platform === 'linux' || platform === 'alpine') { + if (platform === 'linux' || platform === 'alpine') { result = es.merge(result, gulp.src(`resources/server/bin/helpers/check-requirements-linux.sh`, { base: '.' }) .pipe(rename(`bin/helpers/check-requirements.sh`)) diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index a63f693c95a..0624f5f5a67 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -372,8 +372,9 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op const shortcut = gulp.src('resources/darwin/bin/code.sh') .pipe(replace('@@APPNAME@@', product.applicationName)) .pipe(rename('bin/code')); - - all = es.merge(all, shortcut); + const policyDest = gulp.src('.build/policies/darwin/**', { base: '.build/policies/darwin' }) + .pipe(rename(f => f.dirname = `policies/${f.dirname}`)); + all = es.merge(all, shortcut, policyDest); } let result = all diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 5cf5c58402c..2cab7b81832 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -81,7 +81,9 @@ const CORE_TYPES = [ 'ImportMeta', // webcrypto has been available since Node.js 19, but still live in dom.d.ts 'Crypto', - 'SubtleCrypto' + 'SubtleCrypto', + 'JsonWebKey', + 'MessageEvent', ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 63377328928..685dd59cb36 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -80,7 +80,9 @@ const CORE_TYPES = [ // webcrypto has been available since Node.js 19, but still live in dom.d.ts 'Crypto', - 'SubtleCrypto' + 'SubtleCrypto', + 'JsonWebKey', + 'MessageEvent', ]; // Types that are defined in a common layer but are known to be only diff --git a/build/lib/policies.js b/build/lib/policies.js index b76d9ffe00a..bb7e45c1873 100644 --- a/build/lib/policies.js +++ b/build/lib/policies.js @@ -27,7 +27,11 @@ function isNlsStringArray(value) { } var PolicyType; (function (PolicyType) { - PolicyType[PolicyType["StringEnum"] = 0] = "StringEnum"; + PolicyType["Boolean"] = "boolean"; + PolicyType["Number"] = "number"; + PolicyType["Object"] = "object"; + PolicyType["String"] = "string"; + PolicyType["StringEnum"] = "stringEnum"; })(PolicyType || (PolicyType = {})); function renderADMLString(prefix, moduleName, nlsString, translations) { let value; @@ -42,15 +46,28 @@ function renderADMLString(prefix, moduleName, nlsString, translations) { } return `${value}`; } +function renderProfileString(_prefix, moduleName, nlsString, translations) { + let value; + if (translations) { + const moduleTranslations = translations[moduleName]; + if (moduleTranslations) { + value = moduleTranslations[nlsString.nlsKey]; + } + } + if (!value) { + value = nlsString.value; + } + return value; +} class BasePolicy { - policyType; + type; name; category; minimumVersion; description; moduleName; - constructor(policyType, name, category, minimumVersion, description, moduleName) { - this.policyType = policyType; + constructor(type, name, category, minimumVersion, description, moduleName) { + this.type = type; this.name = name; this.category = category; this.minimumVersion = minimumVersion; @@ -80,17 +97,25 @@ class BasePolicy { renderADMLPresentation() { return `${this.renderADMLPresentationContents()}`; } + renderProfile() { + return [`${this.name}`, this.renderProfileValue()]; + } + renderProfileManifest(translations) { + return ` +${this.renderProfileManifestValue(translations)} +`; + } } class BooleanPolicy extends BasePolicy { static from(name, category, minimumVersion, description, moduleName, settingNode) { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'boolean') { return undefined; } return new BooleanPolicy(name, category, minimumVersion, description, moduleName); } constructor(name, category, minimumVersion, description, moduleName) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.Boolean, name, category, minimumVersion, description, moduleName); } renderADMXElements() { return [ @@ -102,19 +127,39 @@ class BooleanPolicy extends BasePolicy { renderADMLPresentationContents() { return `${this.name}`; } + renderProfileValue() { + return ``; + } + renderProfileManifestValue(translations) { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +boolean`; + } } -class IntPolicy extends BasePolicy { +class ParseError extends Error { + constructor(message, moduleName, node) { + super(`${message}. ${moduleName}.ts:${node.startPosition.row + 1}`); + } +} +class NumberPolicy extends BasePolicy { defaultValue; static from(name, category, minimumVersion, description, moduleName, settingNode) { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'number') { return undefined; } - const defaultValue = getIntProperty(settingNode, 'default'); + const defaultValue = getNumberProperty(moduleName, settingNode, 'default'); if (typeof defaultValue === 'undefined') { - throw new Error(`Missing required 'default' property.`); + throw new ParseError(`Missing required 'default' property.`, moduleName, settingNode); } - return new IntPolicy(name, category, minimumVersion, description, moduleName, defaultValue); + return new NumberPolicy(name, category, minimumVersion, description, moduleName, defaultValue); } constructor(name, category, minimumVersion, description, moduleName, defaultValue) { super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); @@ -129,17 +174,32 @@ class IntPolicy extends BasePolicy { renderADMLPresentationContents() { return `${this.name}`; } + renderProfileValue() { + return `${this.defaultValue}`; + } + renderProfileManifestValue(translations) { + return `pfm_default +${this.defaultValue} +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +integer`; + } } class StringPolicy extends BasePolicy { static from(name, category, minimumVersion, description, moduleName, settingNode) { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'string') { return undefined; } return new StringPolicy(name, category, minimumVersion, description, moduleName); } constructor(name, category, minimumVersion, description, moduleName) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.String, name, category, minimumVersion, description, moduleName); } renderADMXElements() { return [``]; @@ -147,17 +207,32 @@ class StringPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + renderProfileValue() { + return ``; + } + renderProfileManifestValue(translations) { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string`; + } } class ObjectPolicy extends BasePolicy { static from(name, category, minimumVersion, description, moduleName, settingNode) { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'object' && type !== 'array') { return undefined; } return new ObjectPolicy(name, category, minimumVersion, description, moduleName); } constructor(name, category, minimumVersion, description, moduleName) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.Object, name, category, minimumVersion, description, moduleName); } renderADMXElements() { return [``]; @@ -165,28 +240,44 @@ class ObjectPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + renderProfileValue() { + return ``; + } + renderProfileManifestValue(translations) { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string +`; + } } class StringEnumPolicy extends BasePolicy { enum_; enumDescriptions; static from(name, category, minimumVersion, description, moduleName, settingNode) { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'string') { return undefined; } - const enum_ = getStringArrayProperty(settingNode, 'enum'); + const enum_ = getStringArrayProperty(moduleName, settingNode, 'enum'); if (!enum_) { return undefined; } if (!isStringArray(enum_)) { - throw new Error(`Property 'enum' should not be localized.`); + throw new ParseError(`Property 'enum' should not be localized.`, moduleName, settingNode); } - const enumDescriptions = getStringArrayProperty(settingNode, 'enumDescriptions'); + const enumDescriptions = getStringArrayProperty(moduleName, settingNode, 'enumDescriptions'); if (!enumDescriptions) { - throw new Error(`Missing required 'enumDescriptions' property.`); + throw new ParseError(`Missing required 'enumDescriptions' property.`, moduleName, settingNode); } else if (!isNlsStringArray(enumDescriptions)) { - throw new Error(`Property 'enumDescriptions' should be localized.`); + throw new ParseError(`Property 'enumDescriptions' should be localized.`, moduleName, settingNode); } return new StringEnumPolicy(name, category, minimumVersion, description, moduleName, enum_, enumDescriptions); } @@ -211,8 +302,27 @@ class StringEnumPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + renderProfileValue() { + return `${this.enum_[0]}`; + } + renderProfileManifestValue(translations) { + return `pfm_default +${this.enum_[0]} +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string +pfm_range_list + + ${this.enum_.map(e => `${e}`).join('\n ')} +`; + } } -const IntQ = { +const NumberQ = { Q: `(number) @value`, value(matches) { const match = matches[0]; @@ -260,47 +370,52 @@ const StringArrayQ = { }); } }; -function getProperty(qtype, node, key) { +function getProperty(qtype, moduleName, node, key) { const query = new tree_sitter_1.default.Query(typescript, `( (pair key: [(property_identifier)(string)] @key value: ${qtype.Q} ) - (#eq? @key ${key}) + (#any-of? @key "${key}" "'${key}'") )`); - return qtype.value(query.matches(node)); + try { + return qtype.value(query.matches(node)); + } + catch (e) { + throw new ParseError(e.message, moduleName, node); + } } -function getIntProperty(node, key) { - return getProperty(IntQ, node, key); +function getNumberProperty(moduleName, node, key) { + return getProperty(NumberQ, moduleName, node, key); } -function getStringProperty(node, key) { - return getProperty(StringQ, node, key); +function getStringProperty(moduleName, node, key) { + return getProperty(StringQ, moduleName, node, key); } -function getStringArrayProperty(node, key) { - return getProperty(StringArrayQ, node, key); +function getStringArrayProperty(moduleName, node, key) { + return getProperty(StringArrayQ, moduleName, node, key); } // TODO: add more policy types const PolicyTypes = [ BooleanPolicy, - IntPolicy, + NumberPolicy, StringEnumPolicy, StringPolicy, ObjectPolicy ]; function getPolicy(moduleName, configurationNode, settingNode, policyNode, categories) { - const name = getStringProperty(policyNode, 'name'); + const name = getStringProperty(moduleName, policyNode, 'name'); if (!name) { - throw new Error(`Missing required 'name' property.`); + throw new ParseError(`Missing required 'name' property`, moduleName, policyNode); } else if (isNlsString(name)) { - throw new Error(`Property 'name' should be a literal string.`); + throw new ParseError(`Property 'name' should be a literal string`, moduleName, policyNode); } - const categoryName = getStringProperty(configurationNode, 'title'); + const categoryName = getStringProperty(moduleName, configurationNode, 'title'); if (!categoryName) { - throw new Error(`Missing required 'title' property.`); + throw new ParseError(`Missing required 'title' property`, moduleName, configurationNode); } else if (!isNlsString(categoryName)) { - throw new Error(`Property 'title' should be localized.`); + throw new ParseError(`Property 'title' should be localized`, moduleName, configurationNode); } const categoryKey = `${categoryName.nlsKey}:${categoryName.value}`; let category = categories.get(categoryKey); @@ -308,19 +423,19 @@ function getPolicy(moduleName, configurationNode, settingNode, policyNode, categ category = { moduleName, name: categoryName }; categories.set(categoryKey, category); } - const minimumVersion = getStringProperty(policyNode, 'minimumVersion'); + const minimumVersion = getStringProperty(moduleName, policyNode, 'minimumVersion'); if (!minimumVersion) { - throw new Error(`Missing required 'minimumVersion' property.`); + throw new ParseError(`Missing required 'minimumVersion' property.`, moduleName, policyNode); } else if (isNlsString(minimumVersion)) { - throw new Error(`Property 'minimumVersion' should be a literal string.`); + throw new ParseError(`Property 'minimumVersion' should be a literal string.`, moduleName, policyNode); } - const description = getStringProperty(settingNode, 'description'); + const description = getStringProperty(moduleName, policyNode, 'description') ?? getStringProperty(moduleName, settingNode, 'description'); if (!description) { - throw new Error(`Missing required 'description' property.`); + throw new ParseError(`Missing required 'description' property.`, moduleName, settingNode); } if (!isNlsString(description)) { - throw new Error(`Property 'description' should be localized.`); + throw new ParseError(`Property 'description' should be localized.`, moduleName, settingNode); } let result; for (const policyType of PolicyTypes) { @@ -329,7 +444,7 @@ function getPolicy(moduleName, configurationNode, settingNode, policyNode, categ } } if (!result) { - throw new Error(`Failed to parse policy '${name}'.`); + throw new ParseError(`Failed to parse policy '${name}'.`, moduleName, settingNode); } return result; } @@ -339,11 +454,11 @@ function getPolicies(moduleName, node) { (call_expression function: (member_expression property: (property_identifier) @registerConfigurationFn) (#eq? @registerConfigurationFn registerConfiguration) arguments: (arguments (object (pair - key: [(property_identifier)(string)] @propertiesKey (#eq? @propertiesKey properties) + key: [(property_identifier)(string)] @propertiesKey (#any-of? @propertiesKey "properties" "'properties'") value: (object (pair key: [(property_identifier)(string)(computed_property_name)] value: (object (pair - key: [(property_identifier)(string)] @policyKey (#eq? @policyKey policy) + key: [(property_identifier)(string)] @policyKey (#any-of? @policyKey "policy" "'policy'") value: (object) @policy )) @setting )) @@ -411,6 +526,186 @@ function renderADML(appName, versions, categories, policies, translations) { `; } +function renderProfileManifest(appName, bundleIdentifier, _versions, _categories, policies, translations) { + const requiredPayloadFields = ` + + pfm_default + Configure ${appName} + pfm_name + PayloadDescription + pfm_title + Payload Description + pfm_type + string + + + pfm_default + ${appName} + pfm_name + PayloadDisplayName + pfm_require + always + pfm_title + Payload Display Name + pfm_type + string + + + pfm_default + ${bundleIdentifier} + pfm_name + PayloadIdentifier + pfm_require + always + pfm_title + Payload Identifier + pfm_type + string + + + pfm_default + ${bundleIdentifier} + pfm_name + PayloadType + pfm_require + always + pfm_title + Payload Type + pfm_type + string + + + pfm_default + + pfm_name + PayloadUUID + pfm_require + always + pfm_title + Payload UUID + pfm_type + string + + + pfm_default + 1 + pfm_name + PayloadVersion + pfm_range_list + + 1 + + pfm_require + always + pfm_title + Payload Version + pfm_type + integer + + + pfm_default + Microsoft + pfm_name + PayloadOrganization + pfm_title + Payload Organization + pfm_type + string + `; + const profileManifestSubkeys = policies.map(policy => { + return policy.renderProfileManifest(translations); + }).join(''); + return ` + + + + pfm_app_url + https://code.visualstudio.com/ + pfm_description + ${appName} Managed Settings + pfm_documentation_url + https://code.visualstudio.com/docs/setup/enterprise + pfm_domain + ${bundleIdentifier} + pfm_format_version + 1 + pfm_interaction + combined + pfm_last_modified + ${new Date().toISOString().replace(/\.\d+Z$/, 'Z')} + pfm_platforms + + macOS + + pfm_subkeys + + ${requiredPayloadFields} + ${profileManifestSubkeys} + + pfm_title + ${appName} + pfm_unique + + pfm_version + 1 + +`; +} +function renderMacOSPolicy(policies, translations) { + const appName = product.nameLong; + const bundleIdentifier = product.darwinBundleIdentifier; + const payloadUUID = product.darwinProfilePayloadUUID; + const UUID = product.darwinProfileUUID; + const versions = [...new Set(policies.map(p => p.minimumVersion)).values()].sort(); + const categories = [...new Set(policies.map(p => p.category))]; + const policyEntries = policies.map(policy => policy.renderProfile()) + .flat() + .map(entry => `\t\t\t\t${entry}`) + .join('\n'); + return { + profile: ` + + + + PayloadContent + + + PayloadDisplayName + ${appName} + PayloadIdentifier + ${bundleIdentifier}.${UUID} + PayloadType + ${bundleIdentifier} + PayloadUUID + ${UUID} + PayloadVersion + 1 +${policyEntries} + + + PayloadDescription + This profile manages ${appName}. For more information see https://code.visualstudio.com/docs/setup/enterprise + PayloadDisplayName + ${appName} + PayloadIdentifier + ${bundleIdentifier} + PayloadOrganization + Microsoft + PayloadType + Configuration + PayloadUUID + ${payloadUUID} + PayloadVersion + 1 + TargetDeviceType + 5 + +`, + manifests: [{ languageId: 'en-us', contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies) }, + ...translations.map(({ languageId, languageTranslations }) => ({ languageId, contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies, languageTranslations) })) + ] + }; +} function renderGP(policies, translations) { const appName = product.nameLong; const regKey = product.win32RegValueName; @@ -526,10 +821,9 @@ async function getTranslations() { return await Promise.all(languageIds.map(languageId => getNLS(extensionGalleryServiceUrl, resourceUrlTemplate, languageId, version) .then(languageTranslations => ({ languageId, languageTranslations })))); } -async function main() { - const [policies, translations] = await Promise.all([parsePolicies(), getTranslations()]); - const { admx, adml } = await renderGP(policies, translations); +async function windowsMain(policies, translations) { const root = '.build/policies/win32'; + const { admx, adml } = await renderGP(policies, translations); await fs_1.promises.rm(root, { recursive: true, force: true }); await fs_1.promises.mkdir(root, { recursive: true }); await fs_1.promises.writeFile(path_1.default.join(root, `${product.win32RegValueName}.admx`), admx.replace(/\r?\n/g, '\n')); @@ -539,9 +833,44 @@ async function main() { await fs_1.promises.writeFile(path_1.default.join(languagePath, `${product.win32RegValueName}.adml`), contents.replace(/\r?\n/g, '\n')); } } +async function darwinMain(policies, translations) { + const bundleIdentifier = product.darwinBundleIdentifier; + if (!bundleIdentifier || !product.darwinProfilePayloadUUID || !product.darwinProfileUUID) { + throw new Error(`Missing required product information.`); + } + const root = '.build/policies/darwin'; + const { profile, manifests } = await renderMacOSPolicy(policies, translations); + await fs_1.promises.rm(root, { recursive: true, force: true }); + await fs_1.promises.mkdir(root, { recursive: true }); + await fs_1.promises.writeFile(path_1.default.join(root, `${bundleIdentifier}.mobileconfig`), profile.replace(/\r?\n/g, '\n')); + for (const { languageId, contents } of manifests) { + const languagePath = path_1.default.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId]); + await fs_1.promises.mkdir(languagePath, { recursive: true }); + await fs_1.promises.writeFile(path_1.default.join(languagePath, `${bundleIdentifier}.plist`), contents.replace(/\r?\n/g, '\n')); + } +} +async function main() { + const [policies, translations] = await Promise.all([parsePolicies(), getTranslations()]); + const platform = process.argv[2]; + if (platform === 'darwin') { + await darwinMain(policies, translations); + } + else if (platform === 'win32') { + await windowsMain(policies, translations); + } + else { + console.error(`Usage: node build/lib/policies `); + process.exit(1); + } +} if (require.main === module) { main().catch(err => { - console.error(err); + if (err instanceof ParseError) { + console.error(`Parse Error:`, err.message); + } + else { + console.error(err); + } process.exit(1); }); } diff --git a/build/lib/policies.ts b/build/lib/policies.ts index 2488920ce26..cf16ca2d511 100644 --- a/build/lib/policies.ts +++ b/build/lib/policies.ts @@ -33,15 +33,24 @@ interface Category { } enum PolicyType { - StringEnum + Boolean = 'boolean', + Number = 'number', + Object = 'object', + String = 'string', + StringEnum = 'stringEnum', } interface Policy { + readonly name: string; + readonly type: PolicyType; readonly category: Category; readonly minimumVersion: string; renderADMX(regKey: string): string[]; renderADMLStrings(translations?: LanguageTranslations): string[]; renderADMLPresentation(): string; + renderProfile(): string[]; + // https://github.com/ProfileManifests/ProfileManifests/wiki/Manifest-Format + renderProfileManifest(translations?: LanguageTranslations): string; } function renderADMLString(prefix: string, moduleName: string, nlsString: NlsString, translations?: LanguageTranslations): string { @@ -62,10 +71,28 @@ function renderADMLString(prefix: string, moduleName: string, nlsString: NlsStri return `${value}`; } +function renderProfileString(_prefix: string, moduleName: string, nlsString: NlsString, translations?: LanguageTranslations): string { + let value: string | undefined; + + if (translations) { + const moduleTranslations = translations[moduleName]; + + if (moduleTranslations) { + value = moduleTranslations[nlsString.nlsKey]; + } + } + + if (!value) { + value = nlsString.value; + } + + return value; +} + abstract class BasePolicy implements Policy { constructor( - protected policyType: PolicyType, - protected name: string, + readonly type: PolicyType, + readonly name: string, readonly category: Category, readonly minimumVersion: string, protected description: NlsString, @@ -102,6 +129,19 @@ abstract class BasePolicy implements Policy { } protected abstract renderADMLPresentationContents(): string; + + renderProfile() { + return [`${this.name}`, this.renderProfileValue()]; + } + + renderProfileManifest(translations?: LanguageTranslations): string { + return ` +${this.renderProfileManifestValue(translations)} +`; + } + + abstract renderProfileValue(): string; + abstract renderProfileManifestValue(translations?: LanguageTranslations): string; } class BooleanPolicy extends BasePolicy { @@ -114,7 +154,7 @@ class BooleanPolicy extends BasePolicy { moduleName: string, settingNode: Parser.SyntaxNode ): BooleanPolicy | undefined { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'boolean') { return undefined; @@ -130,7 +170,7 @@ class BooleanPolicy extends BasePolicy { description: NlsString, moduleName: string, ) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.Boolean, name, category, minimumVersion, description, moduleName); } protected renderADMXElements(): string[] { @@ -144,9 +184,32 @@ class BooleanPolicy extends BasePolicy { renderADMLPresentationContents() { return `${this.name}`; } + + renderProfileValue(): string { + return ``; + } + + renderProfileManifestValue(translations?: LanguageTranslations): string { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +boolean`; + } } -class IntPolicy extends BasePolicy { +class ParseError extends Error { + constructor(message: string, moduleName: string, node: Parser.SyntaxNode) { + super(`${message}. ${moduleName}.ts:${node.startPosition.row + 1}`); + } +} + +class NumberPolicy extends BasePolicy { static from( name: string, @@ -155,20 +218,20 @@ class IntPolicy extends BasePolicy { description: NlsString, moduleName: string, settingNode: Parser.SyntaxNode - ): IntPolicy | undefined { - const type = getStringProperty(settingNode, 'type'); + ): NumberPolicy | undefined { + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'number') { return undefined; } - const defaultValue = getIntProperty(settingNode, 'default'); + const defaultValue = getNumberProperty(moduleName, settingNode, 'default'); if (typeof defaultValue === 'undefined') { - throw new Error(`Missing required 'default' property.`); + throw new ParseError(`Missing required 'default' property.`, moduleName, settingNode); } - return new IntPolicy(name, category, minimumVersion, description, moduleName, defaultValue); + return new NumberPolicy(name, category, minimumVersion, description, moduleName, defaultValue); } private constructor( @@ -192,6 +255,23 @@ class IntPolicy extends BasePolicy { renderADMLPresentationContents() { return `${this.name}`; } + + renderProfileValue() { + return `${this.defaultValue}`; + } + + renderProfileManifestValue(translations?: LanguageTranslations) { + return `pfm_default +${this.defaultValue} +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +integer`; + } } class StringPolicy extends BasePolicy { @@ -204,7 +284,7 @@ class StringPolicy extends BasePolicy { moduleName: string, settingNode: Parser.SyntaxNode ): StringPolicy | undefined { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'string') { return undefined; @@ -220,7 +300,7 @@ class StringPolicy extends BasePolicy { description: NlsString, moduleName: string, ) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.String, name, category, minimumVersion, description, moduleName); } protected renderADMXElements(): string[] { @@ -230,6 +310,23 @@ class StringPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + + renderProfileValue(): string { + return ``; + } + + renderProfileManifestValue(translations?: LanguageTranslations): string { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string`; + } } class ObjectPolicy extends BasePolicy { @@ -242,7 +339,7 @@ class ObjectPolicy extends BasePolicy { moduleName: string, settingNode: Parser.SyntaxNode ): ObjectPolicy | undefined { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'object' && type !== 'array') { return undefined; @@ -258,7 +355,7 @@ class ObjectPolicy extends BasePolicy { description: NlsString, moduleName: string, ) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.Object, name, category, minimumVersion, description, moduleName); } protected renderADMXElements(): string[] { @@ -268,6 +365,24 @@ class ObjectPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + + renderProfileValue(): string { + return ``; + } + + renderProfileManifestValue(translations?: LanguageTranslations): string { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string +`; + } } class StringEnumPolicy extends BasePolicy { @@ -280,28 +395,28 @@ class StringEnumPolicy extends BasePolicy { moduleName: string, settingNode: Parser.SyntaxNode ): StringEnumPolicy | undefined { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'string') { return undefined; } - const enum_ = getStringArrayProperty(settingNode, 'enum'); + const enum_ = getStringArrayProperty(moduleName, settingNode, 'enum'); if (!enum_) { return undefined; } if (!isStringArray(enum_)) { - throw new Error(`Property 'enum' should not be localized.`); + throw new ParseError(`Property 'enum' should not be localized.`, moduleName, settingNode); } - const enumDescriptions = getStringArrayProperty(settingNode, 'enumDescriptions'); + const enumDescriptions = getStringArrayProperty(moduleName, settingNode, 'enumDescriptions'); if (!enumDescriptions) { - throw new Error(`Missing required 'enumDescriptions' property.`); + throw new ParseError(`Missing required 'enumDescriptions' property.`, moduleName, settingNode); } else if (!isNlsStringArray(enumDescriptions)) { - throw new Error(`Property 'enumDescriptions' should be localized.`); + throw new ParseError(`Property 'enumDescriptions' should be localized.`, moduleName, settingNode); } return new StringEnumPolicy(name, category, minimumVersion, description, moduleName, enum_, enumDescriptions); @@ -337,6 +452,27 @@ class StringEnumPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + + renderProfileValue() { + return `${this.enum_[0]}`; + } + + renderProfileManifestValue(translations?: LanguageTranslations): string { + return `pfm_default +${this.enum_[0]} +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string +pfm_range_list + + ${this.enum_.map(e => `${e}`).join('\n ')} +`; + } } interface QType { @@ -344,7 +480,7 @@ interface QType { value(matches: Parser.QueryMatch[]): T | undefined; } -const IntQ: QType = { +const NumberQ: QType = { Q: `(number) @value`, value(matches: Parser.QueryMatch[]): number | undefined { @@ -407,7 +543,7 @@ const StringArrayQ: QType<(string | NlsString)[]> = { } }; -function getProperty(qtype: QType, node: Parser.SyntaxNode, key: string): T | undefined { +function getProperty(qtype: QType, moduleName: string, node: Parser.SyntaxNode, key: string): T | undefined { const query = new Parser.Query( typescript, `( @@ -415,29 +551,33 @@ function getProperty(qtype: QType, node: Parser.SyntaxNode, key: string): key: [(property_identifier)(string)] @key value: ${qtype.Q} ) - (#eq? @key ${key}) + (#any-of? @key "${key}" "'${key}'") )` ); - return qtype.value(query.matches(node)); + try { + return qtype.value(query.matches(node)); + } catch (e) { + throw new ParseError(e.message, moduleName, node); + } } -function getIntProperty(node: Parser.SyntaxNode, key: string): number | undefined { - return getProperty(IntQ, node, key); +function getNumberProperty(moduleName: string, node: Parser.SyntaxNode, key: string): number | undefined { + return getProperty(NumberQ, moduleName, node, key); } -function getStringProperty(node: Parser.SyntaxNode, key: string): string | NlsString | undefined { - return getProperty(StringQ, node, key); +function getStringProperty(moduleName: string, node: Parser.SyntaxNode, key: string): string | NlsString | undefined { + return getProperty(StringQ, moduleName, node, key); } -function getStringArrayProperty(node: Parser.SyntaxNode, key: string): (string | NlsString)[] | undefined { - return getProperty(StringArrayQ, node, key); +function getStringArrayProperty(moduleName: string, node: Parser.SyntaxNode, key: string): (string | NlsString)[] | undefined { + return getProperty(StringArrayQ, moduleName, node, key); } // TODO: add more policy types const PolicyTypes = [ BooleanPolicy, - IntPolicy, + NumberPolicy, StringEnumPolicy, StringPolicy, ObjectPolicy @@ -450,20 +590,20 @@ function getPolicy( policyNode: Parser.SyntaxNode, categories: Map ): Policy { - const name = getStringProperty(policyNode, 'name'); + const name = getStringProperty(moduleName, policyNode, 'name'); if (!name) { - throw new Error(`Missing required 'name' property.`); + throw new ParseError(`Missing required 'name' property`, moduleName, policyNode); } else if (isNlsString(name)) { - throw new Error(`Property 'name' should be a literal string.`); + throw new ParseError(`Property 'name' should be a literal string`, moduleName, policyNode); } - const categoryName = getStringProperty(configurationNode, 'title'); + const categoryName = getStringProperty(moduleName, configurationNode, 'title'); if (!categoryName) { - throw new Error(`Missing required 'title' property.`); + throw new ParseError(`Missing required 'title' property`, moduleName, configurationNode); } else if (!isNlsString(categoryName)) { - throw new Error(`Property 'title' should be localized.`); + throw new ParseError(`Property 'title' should be localized`, moduleName, configurationNode); } const categoryKey = `${categoryName.nlsKey}:${categoryName.value}`; @@ -474,20 +614,20 @@ function getPolicy( categories.set(categoryKey, category); } - const minimumVersion = getStringProperty(policyNode, 'minimumVersion'); + const minimumVersion = getStringProperty(moduleName, policyNode, 'minimumVersion'); if (!minimumVersion) { - throw new Error(`Missing required 'minimumVersion' property.`); + throw new ParseError(`Missing required 'minimumVersion' property.`, moduleName, policyNode); } else if (isNlsString(minimumVersion)) { - throw new Error(`Property 'minimumVersion' should be a literal string.`); + throw new ParseError(`Property 'minimumVersion' should be a literal string.`, moduleName, policyNode); } - const description = getStringProperty(settingNode, 'description'); + const description = getStringProperty(moduleName, policyNode, 'description') ?? getStringProperty(moduleName, settingNode, 'description'); if (!description) { - throw new Error(`Missing required 'description' property.`); + throw new ParseError(`Missing required 'description' property.`, moduleName, settingNode); } if (!isNlsString(description)) { - throw new Error(`Property 'description' should be localized.`); + throw new ParseError(`Property 'description' should be localized.`, moduleName, settingNode); } let result: Policy | undefined; @@ -499,7 +639,7 @@ function getPolicy( } if (!result) { - throw new Error(`Failed to parse policy '${name}'.`); + throw new ParseError(`Failed to parse policy '${name}'.`, moduleName, settingNode); } return result; @@ -511,11 +651,11 @@ function getPolicies(moduleName: string, node: Parser.SyntaxNode): Policy[] { (call_expression function: (member_expression property: (property_identifier) @registerConfigurationFn) (#eq? @registerConfigurationFn registerConfiguration) arguments: (arguments (object (pair - key: [(property_identifier)(string)] @propertiesKey (#eq? @propertiesKey properties) + key: [(property_identifier)(string)] @propertiesKey (#any-of? @propertiesKey "properties" "'properties'") value: (object (pair key: [(property_identifier)(string)(computed_property_name)] value: (object (pair - key: [(property_identifier)(string)] @policyKey (#eq? @policyKey policy) + key: [(property_identifier)(string)] @policyKey (#any-of? @policyKey "policy" "'policy'") value: (object) @policy )) @setting )) @@ -590,6 +730,197 @@ function renderADML(appName: string, versions: string[], categories: Category[], `; } +function renderProfileManifest(appName: string, bundleIdentifier: string, _versions: string[], _categories: Category[], policies: Policy[], translations?: LanguageTranslations) { + + const requiredPayloadFields = ` + + pfm_default + Configure ${appName} + pfm_name + PayloadDescription + pfm_title + Payload Description + pfm_type + string + + + pfm_default + ${appName} + pfm_name + PayloadDisplayName + pfm_require + always + pfm_title + Payload Display Name + pfm_type + string + + + pfm_default + ${bundleIdentifier} + pfm_name + PayloadIdentifier + pfm_require + always + pfm_title + Payload Identifier + pfm_type + string + + + pfm_default + ${bundleIdentifier} + pfm_name + PayloadType + pfm_require + always + pfm_title + Payload Type + pfm_type + string + + + pfm_default + + pfm_name + PayloadUUID + pfm_require + always + pfm_title + Payload UUID + pfm_type + string + + + pfm_default + 1 + pfm_name + PayloadVersion + pfm_range_list + + 1 + + pfm_require + always + pfm_title + Payload Version + pfm_type + integer + + + pfm_default + Microsoft + pfm_name + PayloadOrganization + pfm_title + Payload Organization + pfm_type + string + `; + + const profileManifestSubkeys = policies.map(policy => { + return policy.renderProfileManifest(translations); + }).join(''); + + return ` + + + + pfm_app_url + https://code.visualstudio.com/ + pfm_description + ${appName} Managed Settings + pfm_documentation_url + https://code.visualstudio.com/docs/setup/enterprise + pfm_domain + ${bundleIdentifier} + pfm_format_version + 1 + pfm_interaction + combined + pfm_last_modified + ${new Date().toISOString().replace(/\.\d+Z$/, 'Z')} + pfm_platforms + + macOS + + pfm_subkeys + + ${requiredPayloadFields} + ${profileManifestSubkeys} + + pfm_title + ${appName} + pfm_unique + + pfm_version + 1 + +`; +} + +function renderMacOSPolicy(policies: Policy[], translations: Translations) { + const appName = product.nameLong; + const bundleIdentifier = product.darwinBundleIdentifier; + const payloadUUID = product.darwinProfilePayloadUUID; + const UUID = product.darwinProfileUUID; + + const versions = [...new Set(policies.map(p => p.minimumVersion)).values()].sort(); + const categories = [...new Set(policies.map(p => p.category))]; + + const policyEntries = + policies.map(policy => policy.renderProfile()) + .flat() + .map(entry => `\t\t\t\t${entry}`) + .join('\n'); + + + return { + profile: ` + + + + PayloadContent + + + PayloadDisplayName + ${appName} + PayloadIdentifier + ${bundleIdentifier}.${UUID} + PayloadType + ${bundleIdentifier} + PayloadUUID + ${UUID} + PayloadVersion + 1 +${policyEntries} + + + PayloadDescription + This profile manages ${appName}. For more information see https://code.visualstudio.com/docs/setup/enterprise + PayloadDisplayName + ${appName} + PayloadIdentifier + ${bundleIdentifier} + PayloadOrganization + Microsoft + PayloadType + Configuration + PayloadUUID + ${payloadUUID} + PayloadVersion + 1 + TargetDeviceType + 5 + +`, + manifests: [{ languageId: 'en-us', contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies) }, + ...translations.map(({ languageId, languageTranslations }) => + ({ languageId, contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies, languageTranslations) })) + ] + }; +} + function renderGP(policies: Policy[], translations: Translations) { const appName = product.nameLong; const regKey = product.win32RegValueName; @@ -735,11 +1066,10 @@ async function getTranslations(): Promise { )); } -async function main() { - const [policies, translations] = await Promise.all([parsePolicies(), getTranslations()]); +async function windowsMain(policies: Policy[], translations: Translations) { + const root = '.build/policies/win32'; const { admx, adml } = await renderGP(policies, translations); - const root = '.build/policies/win32'; await fs.rm(root, { recursive: true, force: true }); await fs.mkdir(root, { recursive: true }); @@ -752,9 +1082,46 @@ async function main() { } } +async function darwinMain(policies: Policy[], translations: Translations) { + const bundleIdentifier = product.darwinBundleIdentifier; + if (!bundleIdentifier || !product.darwinProfilePayloadUUID || !product.darwinProfileUUID) { + throw new Error(`Missing required product information.`); + } + const root = '.build/policies/darwin'; + const { profile, manifests } = await renderMacOSPolicy(policies, translations); + + await fs.rm(root, { recursive: true, force: true }); + await fs.mkdir(root, { recursive: true }); + await fs.writeFile(path.join(root, `${bundleIdentifier}.mobileconfig`), profile.replace(/\r?\n/g, '\n')); + + for (const { languageId, contents } of manifests) { + const languagePath = path.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId as keyof typeof Languages]); + await fs.mkdir(languagePath, { recursive: true }); + await fs.writeFile(path.join(languagePath, `${bundleIdentifier}.plist`), contents.replace(/\r?\n/g, '\n')); + } +} + +async function main() { + const [policies, translations] = await Promise.all([parsePolicies(), getTranslations()]); + const platform = process.argv[2]; + + if (platform === 'darwin') { + await darwinMain(policies, translations); + } else if (platform === 'win32') { + await windowsMain(policies, translations); + } else { + console.error(`Usage: node build/lib/policies `); + process.exit(1); + } +} + if (require.main === module) { main().catch(err => { - console.error(err); + if (err instanceof ParseError) { + console.error(`Parse Error:`, err.message); + } else { + console.error(err); + } process.exit(1); }); } diff --git a/build/lib/propertyInitOrderChecker.js b/build/lib/propertyInitOrderChecker.js new file mode 100644 index 00000000000..dbca887bc22 --- /dev/null +++ b/build/lib/propertyInitOrderChecker.js @@ -0,0 +1,367 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.EntryKind = void 0; +const ts = __importStar(require("typescript")); +const path = __importStar(require("path")); +const fs = __importStar(require("fs")); +const TS_CONFIG_PATH = path.join(__dirname, '../../', 'src', 'tsconfig.json'); +// +// ############################################################################################# +// +// A custom typescript checker that ensure constructor properties are NOT used to initialize +// defined properties. This is needed for the times when `useDefineForClassFields` is gone. +// +// see https://github.com/microsoft/vscode/issues/243049, https://github.com/microsoft/vscode/issues/186726, +// https://github.com/microsoft/vscode/pull/241544 +// +// ############################################################################################# +// +const ignored = new Set([ + 'vs/base/common/arrays.ts', + 'vs/platform/extensionManagement/common/extensionsScannerService.ts', + 'vs/platform/configuration/common/configurations.ts', + 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts', + 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts', + 'vs/editor/common/model/textModelTokens.ts', + 'vs/editor/common/model/tokenizationTextModelPart.ts', + 'vs/editor/common/core/textEdit.ts', + 'vs/workbench/contrib/debug/common/debugStorage.ts', + 'vs/workbench/contrib/debug/common/debugModel.ts', + 'vs/workbench/api/common/extHostCommands.ts', + 'vs/editor/browser/view/viewLayer.ts', + 'vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts', + 'vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts', + 'vs/editor/browser/widget/diffEditor/utils.ts', + 'vs/editor/browser/observableCodeEditor.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts', + 'vs/editor/browser/widget/diffEditor/diffEditorOptions.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts', + 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts', + 'vs/editor/browser/widget/diffEditor/utils/editorGutter.ts', + 'vs/editor/browser/widget/diffEditor/features/gutterFeature.ts', + 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts', + 'vs/editor/browser/widget/diffEditor/diffEditorWidget.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts', + 'vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts', + 'vs/editor/contrib/inlayHints/browser/inlayHintsController.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts', + 'vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts', + 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts', + 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts', + 'vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts', + 'vs/workbench/contrib/files/browser/views/openEditorsView.ts', + 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts', + 'vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts', + 'vs/workbench/contrib/chat/browser/chatInputPart.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts', + 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts', + 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts', + 'vs/platform/terminal/common/capabilities/commandDetectionCapability.ts', + 'vs/workbench/contrib/testing/common/testExclusions.ts', + 'vs/workbench/contrib/testing/common/testResultStorage.ts', + 'vs/workbench/services/userDataProfile/browser/snippetsResource.ts', + 'vs/platform/quickinput/browser/quickInputController.ts', + 'vs/platform/userDataSync/common/abstractSynchronizer.ts', + 'vs/workbench/services/authentication/browser/authenticationExtensionsService.ts', + 'vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts', + 'vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts', + 'vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts', + 'vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts', + 'vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts', + 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts', + 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts', + 'vs/workbench/contrib/search/common/cacheState.ts', + 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts', + 'vs/workbench/contrib/search/browser/anythingQuickAccess.ts', + 'vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts', + 'vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts', + 'vs/workbench/contrib/testing/common/testExplorerFilterState.ts', + 'vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts', + 'vs/workbench/contrib/testing/browser/testingOutputPeek.ts', + 'vs/workbench/contrib/testing/browser/explorerProjections/index.ts', + 'vs/workbench/contrib/testing/browser/testingExplorerFilter.ts', + 'vs/workbench/contrib/testing/browser/testingExplorerView.ts', + 'vs/workbench/contrib/testing/common/testServiceImpl.ts', + 'vs/platform/quickinput/browser/commandsQuickAccess.ts', + 'vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts', + 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts', + 'vs/workbench/contrib/debug/browser/debugMemory.ts', + 'vs/workbench/contrib/markers/browser/markersViewActions.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts', + 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts', + 'vs/workbench/contrib/output/browser/outputServices.ts', + 'vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts', + 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts', + 'vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts', + 'vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts', + 'vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts', + 'vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts', + 'vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts', + 'vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts', + 'vs/platform/terminal/node/ptyService.ts', + 'vs/workbench/api/common/extHostLanguageFeatures.ts', + 'vs/workbench/api/common/extHostSearch.ts', + 'vs/workbench/contrib/testing/test/common/testStubs.ts' +]); +const cancellationToken = { + isCancellationRequested: () => false, + throwIfCancellationRequested: () => { }, +}; +const seenFiles = new Set(); +let errorCount = 0; +function createProgram(tsconfigPath) { + const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + const configHostParser = { fileExists: fs.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => fs.readFileSync(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; + const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, path.resolve(path.dirname(tsconfigPath)), { noEmit: true }); + const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); + return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); +} +const program = createProgram(TS_CONFIG_PATH); +program.getTypeChecker(); +for (const file of program.getSourceFiles()) { + if (!file || file.isDeclarationFile) { + continue; + } + const relativePath = path.relative(path.dirname(TS_CONFIG_PATH), file.fileName).replace(/\\/g, '/'); + if (ignored.has(relativePath)) { + continue; + } + visit(file); +} +if (seenFiles.size) { + console.log(); + console.log(`Found ${errorCount} error${errorCount === 1 ? '' : 's'} in ${seenFiles.size} file${seenFiles.size === 1 ? '' : 's'}.`); + process.exit(errorCount); +} +function visit(node) { + if (ts.isParameter(node) && ts.isParameterPropertyDeclaration(node, node.parent)) { + checkParameterPropertyDeclaration(node); + } + ts.forEachChild(node, visit); +} +function checkParameterPropertyDeclaration(param) { + const uses = [...collectReferences(param.name, [])]; + if (!uses.length) { + return; + } + const sourceFile = param.getSourceFile(); + if (!seenFiles.has(sourceFile)) { + if (seenFiles.size) { + console.log(``); + } + console.log(`${formatFileName(param)}:`); + seenFiles.add(sourceFile); + } + else { + console.log(``); + } + console.log(` Parameter property '${param.name.getText()}' is used before its declaration.`); + for (const { stack, container } of uses) { + const use = stack[stack.length - 1]; + console.log(` at ${formatLocation(use)}: ${formatMember(container)} -> ${formatStack(stack)}`); + errorCount++; + } +} +function* collectReferences(node, stack, requiresInvocationDepth = 0, seen = new Set()) { + for (const use of findAllReferencesInClass(node)) { + const container = findContainer(use); + if (!container || seen.has(container) || ts.isConstructorDeclaration(container)) { + continue; + } + seen.add(container); + const nextStack = [...stack, use]; + let nextRequiresInvocationDepth = requiresInvocationDepth; + if (isInvocation(use) && nextRequiresInvocationDepth > 0) { + nextRequiresInvocationDepth--; + } + if (ts.isPropertyDeclaration(container) && nextRequiresInvocationDepth === 0) { + yield { stack: nextStack, container }; + } + else if (requiresInvocation(container)) { + nextRequiresInvocationDepth++; + } + yield* collectReferences(container.name ?? container, nextStack, nextRequiresInvocationDepth, seen); + } +} +function requiresInvocation(definition) { + return ts.isMethodDeclaration(definition) || ts.isFunctionDeclaration(definition) || ts.isFunctionExpression(definition) || ts.isArrowFunction(definition); +} +function isInvocation(use) { + let location = use; + if (ts.isPropertyAccessExpression(location.parent) && location.parent.name === location) { + location = location.parent; + } + else if (ts.isElementAccessExpression(location.parent) && location.parent.argumentExpression === location) { + location = location.parent; + } + return ts.isCallExpression(location.parent) && location.parent.expression === location + || ts.isTaggedTemplateExpression(location.parent) && location.parent.tag === location; +} +function formatFileName(node) { + const sourceFile = node.getSourceFile(); + return path.resolve(sourceFile.fileName); +} +function formatLocation(node) { + const sourceFile = node.getSourceFile(); + const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.pos); + return `${formatFileName(sourceFile)}(${line + 1},${character + 1})`; +} +function formatStack(stack) { + return stack.slice().reverse().map((use) => formatUse(use)).join(' -> '); +} +function formatMember(container) { + const name = container.name?.getText(); + if (name) { + const className = findClass(container)?.name?.getText(); + if (className) { + return `${className}.${name}`; + } + return name; + } + return ''; +} +function formatUse(use) { + let text = use.getText(); + if (use.parent && ts.isPropertyAccessExpression(use.parent) && use.parent.name === use) { + if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) { + text = `this.${text}`; + } + use = use.parent; + } + else if (use.parent && ts.isElementAccessExpression(use.parent) && use.parent.argumentExpression === use) { + if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) { + text = `this['${text}']`; + } + use = use.parent; + } + if (ts.isCallExpression(use.parent)) { + text = `${text}(...)`; + } + return text; +} +function findContainer(node) { + return ts.findAncestor(node, ancestor => { + switch (ancestor.kind) { + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ClassStaticBlockDeclaration: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.Parameter: + return true; + } + return false; + }); +} +function findClass(node) { + return ts.findAncestor(node, ts.isClassLike); +} +function* findAllReferencesInClass(node) { + const classDecl = findClass(node); + if (!classDecl) { + return []; + } + for (const ref of findAllReferences(node)) { + for (const entry of ref.references) { + if (entry.kind !== 1 /* EntryKind.Node */ || entry.node === node) { + continue; + } + if (findClass(entry.node) === classDecl) { + yield entry.node; + } + } + } +} +// NOTE: The following uses TypeScript internals and are subject to change from version to version. +function findAllReferences(node) { + const sourceFile = node.getSourceFile(); + const position = node.getStart(); + const name = ts.getTouchingPropertyName(sourceFile, position); + const options = { use: ts.FindAllReferences.FindReferencesUse.References }; + return ts.FindAllReferences.Core.getReferencedSymbolsForNode(position, name, program, [sourceFile], cancellationToken, options) ?? []; +} +var DefinitionKind; +(function (DefinitionKind) { + DefinitionKind[DefinitionKind["Symbol"] = 0] = "Symbol"; + DefinitionKind[DefinitionKind["Label"] = 1] = "Label"; + DefinitionKind[DefinitionKind["Keyword"] = 2] = "Keyword"; + DefinitionKind[DefinitionKind["This"] = 3] = "This"; + DefinitionKind[DefinitionKind["String"] = 4] = "String"; + DefinitionKind[DefinitionKind["TripleSlashReference"] = 5] = "TripleSlashReference"; +})(DefinitionKind || (DefinitionKind = {})); +/** @internal */ +var EntryKind; +(function (EntryKind) { + EntryKind[EntryKind["Span"] = 0] = "Span"; + EntryKind[EntryKind["Node"] = 1] = "Node"; + EntryKind[EntryKind["StringLiteral"] = 2] = "StringLiteral"; + EntryKind[EntryKind["SearchedLocalFoundProperty"] = 3] = "SearchedLocalFoundProperty"; + EntryKind[EntryKind["SearchedPropertyFoundLocal"] = 4] = "SearchedPropertyFoundLocal"; +})(EntryKind || (exports.EntryKind = EntryKind = {})); +//# sourceMappingURL=propertyInitOrderChecker.js.map \ No newline at end of file diff --git a/build/lib/propertyInitOrderChecker.ts b/build/lib/propertyInitOrderChecker.ts new file mode 100644 index 00000000000..dc18213566f --- /dev/null +++ b/build/lib/propertyInitOrderChecker.ts @@ -0,0 +1,417 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import * as ts from 'typescript'; +import * as path from 'path'; +import * as fs from 'fs'; + +const TS_CONFIG_PATH = path.join(__dirname, '../../', 'src', 'tsconfig.json'); + +// +// ############################################################################################# +// +// A custom typescript checker that ensure constructor properties are NOT used to initialize +// defined properties. This is needed for the times when `useDefineForClassFields` is gone. +// +// see https://github.com/microsoft/vscode/issues/243049, https://github.com/microsoft/vscode/issues/186726, +// https://github.com/microsoft/vscode/pull/241544 +// +// ############################################################################################# +// + +const ignored = new Set([ + 'vs/base/common/arrays.ts', + 'vs/platform/extensionManagement/common/extensionsScannerService.ts', + 'vs/platform/configuration/common/configurations.ts', + 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts', + 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts', + 'vs/editor/common/model/textModelTokens.ts', + 'vs/editor/common/model/tokenizationTextModelPart.ts', + 'vs/editor/common/core/textEdit.ts', + 'vs/workbench/contrib/debug/common/debugStorage.ts', + 'vs/workbench/contrib/debug/common/debugModel.ts', + 'vs/workbench/api/common/extHostCommands.ts', + 'vs/editor/browser/view/viewLayer.ts', + 'vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts', + 'vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts', + 'vs/editor/browser/widget/diffEditor/utils.ts', + 'vs/editor/browser/observableCodeEditor.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts', + 'vs/editor/browser/widget/diffEditor/diffEditorOptions.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts', + 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts', + 'vs/editor/browser/widget/diffEditor/utils/editorGutter.ts', + 'vs/editor/browser/widget/diffEditor/features/gutterFeature.ts', + 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts', + 'vs/editor/browser/widget/diffEditor/diffEditorWidget.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts', + 'vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts', + 'vs/editor/contrib/inlayHints/browser/inlayHintsController.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts', + 'vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts', + 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts', + 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts', + 'vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts', + 'vs/workbench/contrib/files/browser/views/openEditorsView.ts', + 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts', + 'vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts', + 'vs/workbench/contrib/chat/browser/chatInputPart.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts', + 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts', + 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts', + 'vs/platform/terminal/common/capabilities/commandDetectionCapability.ts', + 'vs/workbench/contrib/testing/common/testExclusions.ts', + 'vs/workbench/contrib/testing/common/testResultStorage.ts', + 'vs/workbench/services/userDataProfile/browser/snippetsResource.ts', + 'vs/platform/quickinput/browser/quickInputController.ts', + 'vs/platform/userDataSync/common/abstractSynchronizer.ts', + 'vs/workbench/services/authentication/browser/authenticationExtensionsService.ts', + 'vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts', + 'vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts', + 'vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts', + 'vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts', + 'vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts', + 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts', + 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts', + 'vs/workbench/contrib/search/common/cacheState.ts', + 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts', + 'vs/workbench/contrib/search/browser/anythingQuickAccess.ts', + 'vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts', + 'vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts', + 'vs/workbench/contrib/testing/common/testExplorerFilterState.ts', + 'vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts', + 'vs/workbench/contrib/testing/browser/testingOutputPeek.ts', + 'vs/workbench/contrib/testing/browser/explorerProjections/index.ts', + 'vs/workbench/contrib/testing/browser/testingExplorerFilter.ts', + 'vs/workbench/contrib/testing/browser/testingExplorerView.ts', + 'vs/workbench/contrib/testing/common/testServiceImpl.ts', + 'vs/platform/quickinput/browser/commandsQuickAccess.ts', + 'vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts', + 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts', + 'vs/workbench/contrib/debug/browser/debugMemory.ts', + 'vs/workbench/contrib/markers/browser/markersViewActions.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts', + 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts', + 'vs/workbench/contrib/output/browser/outputServices.ts', + 'vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts', + 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts', + 'vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts', + 'vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts', + 'vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts', + 'vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts', + 'vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts', + 'vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts', + 'vs/platform/terminal/node/ptyService.ts', + 'vs/workbench/api/common/extHostLanguageFeatures.ts', + 'vs/workbench/api/common/extHostSearch.ts', + 'vs/workbench/contrib/testing/test/common/testStubs.ts' +]); + + +const cancellationToken: ts.CancellationToken = { + isCancellationRequested: () => false, + throwIfCancellationRequested: () => { }, +}; + +const seenFiles = new Set(); +let errorCount = 0; + + + +function createProgram(tsconfigPath: string): ts.Program { + const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + + const configHostParser: ts.ParseConfigHost = { fileExists: fs.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => fs.readFileSync(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; + const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, path.resolve(path.dirname(tsconfigPath)), { noEmit: true }); + + const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); + + return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); +} + +const program = createProgram(TS_CONFIG_PATH); + +program.getTypeChecker(); + +for (const file of program.getSourceFiles()) { + if (!file || file.isDeclarationFile) { + continue; + } + + const relativePath = path.relative(path.dirname(TS_CONFIG_PATH), file.fileName).replace(/\\/g, '/'); + if (ignored.has(relativePath)) { + continue; + } + + visit(file); +} + +if (seenFiles.size) { + console.log(); + console.log(`Found ${errorCount} error${errorCount === 1 ? '' : 's'} in ${seenFiles.size} file${seenFiles.size === 1 ? '' : 's'}.`); + process.exit(errorCount); +} + +function visit(node: ts.Node) { + if (ts.isParameter(node) && ts.isParameterPropertyDeclaration(node, node.parent)) { + checkParameterPropertyDeclaration(node); + } + + ts.forEachChild(node, visit); +} + +function checkParameterPropertyDeclaration(param: ts.ParameterPropertyDeclaration) { + const uses = [...collectReferences(param.name, [])]; + if (!uses.length) { + return; + } + + const sourceFile = param.getSourceFile(); + if (!seenFiles.has(sourceFile)) { + if (seenFiles.size) { + console.log(``); + } + console.log(`${formatFileName(param)}:`); + seenFiles.add(sourceFile); + } else { + console.log(``); + } + console.log(` Parameter property '${param.name.getText()}' is used before its declaration.`); + for (const { stack, container } of uses) { + const use = stack[stack.length - 1]; + console.log(` at ${formatLocation(use)}: ${formatMember(container)} -> ${formatStack(stack)}`); + errorCount++; + } +} + +interface InvalidUse { + stack: ts.Node[]; + container: ReferenceContainer; +} + +function* collectReferences(node: ts.Node, stack: ts.Node[], requiresInvocationDepth: number = 0, seen = new Set()): Generator { + for (const use of findAllReferencesInClass(node)) { + const container = findContainer(use); + if (!container || seen.has(container) || ts.isConstructorDeclaration(container)) { + continue; + } + seen.add(container); + + const nextStack = [...stack, use]; + + let nextRequiresInvocationDepth = requiresInvocationDepth; + if (isInvocation(use) && nextRequiresInvocationDepth > 0) { + nextRequiresInvocationDepth--; + } + + if (ts.isPropertyDeclaration(container) && nextRequiresInvocationDepth === 0) { + yield { stack: nextStack, container }; + } + else if (requiresInvocation(container)) { + nextRequiresInvocationDepth++; + } + + yield* collectReferences(container.name ?? container, nextStack, nextRequiresInvocationDepth, seen); + } +} + +function requiresInvocation(definition: ReferenceContainer): boolean { + return ts.isMethodDeclaration(definition) || ts.isFunctionDeclaration(definition) || ts.isFunctionExpression(definition) || ts.isArrowFunction(definition); +} + +function isInvocation(use: ts.Node): boolean { + let location = use; + if (ts.isPropertyAccessExpression(location.parent) && location.parent.name === location) { + location = location.parent; + } + else if (ts.isElementAccessExpression(location.parent) && location.parent.argumentExpression === location) { + location = location.parent; + } + return ts.isCallExpression(location.parent) && location.parent.expression === location + || ts.isTaggedTemplateExpression(location.parent) && location.parent.tag === location; +} + +function formatFileName(node: ts.Node): string { + const sourceFile = node.getSourceFile(); + return path.resolve(sourceFile.fileName); +} + +function formatLocation(node: ts.Node): string { + const sourceFile = node.getSourceFile(); + const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.pos); + return `${formatFileName(sourceFile)}(${line + 1},${character + 1})`; +} + +function formatStack(stack: ts.Node[]): string { + return stack.slice().reverse().map((use) => formatUse(use)).join(' -> '); +} + +function formatMember(container: ReferenceContainer): string { + const name = container.name?.getText(); + if (name) { + const className = findClass(container)?.name?.getText(); + if (className) { + return `${className}.${name}`; + } + return name; + } + return ''; +} + +function formatUse(use: ts.Node): string { + let text = use.getText(); + if (use.parent && ts.isPropertyAccessExpression(use.parent) && use.parent.name === use) { + if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) { + text = `this.${text}`; + } + use = use.parent; + } + else if (use.parent && ts.isElementAccessExpression(use.parent) && use.parent.argumentExpression === use) { + if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) { + text = `this['${text}']`; + } + use = use.parent; + } + if (ts.isCallExpression(use.parent)) { + text = `${text}(...)`; + } + return text; +} + +type ReferenceContainer = + | ts.PropertyDeclaration + | ts.MethodDeclaration + | ts.GetAccessorDeclaration + | ts.SetAccessorDeclaration + | ts.ConstructorDeclaration + | ts.ClassStaticBlockDeclaration + | ts.ArrowFunction + | ts.FunctionExpression + | ts.FunctionDeclaration + | ts.ParameterDeclaration; + +function findContainer(node: ts.Node): ReferenceContainer | undefined { + return ts.findAncestor(node, ancestor => { + switch (ancestor.kind) { + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ClassStaticBlockDeclaration: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.Parameter: + return true; + } + return false; + }) as ReferenceContainer | undefined; +} + +function findClass(node: ts.Node): ts.ClassLikeDeclaration | undefined { + return ts.findAncestor(node, ts.isClassLike); +} + +function* findAllReferencesInClass(node: ts.Node): Generator { + const classDecl = findClass(node); + if (!classDecl) { + return []; + } + for (const ref of findAllReferences(node)) { + for (const entry of ref.references) { + if (entry.kind !== EntryKind.Node || entry.node === node) { + continue; + } + if (findClass(entry.node) === classDecl) { + yield entry.node; + } + } + } +} + +// NOTE: The following uses TypeScript internals and are subject to change from version to version. + +function findAllReferences(node: ts.Node): readonly SymbolAndEntries[] { + const sourceFile = node.getSourceFile(); + const position = node.getStart(); + const name: ts.Node = (ts as any).getTouchingPropertyName(sourceFile, position); + const options = { use: (ts as any).FindAllReferences.FindReferencesUse.References }; + return (ts as any).FindAllReferences.Core.getReferencedSymbolsForNode(position, name, program, [sourceFile], cancellationToken, options) ?? []; +} + +interface SymbolAndEntries { + readonly definition: Definition | undefined; + readonly references: readonly Entry[]; +} + +const enum DefinitionKind { + Symbol, + Label, + Keyword, + This, + String, + TripleSlashReference, +} + +type Definition = + | { readonly type: DefinitionKind.Symbol; readonly symbol: ts.Symbol } + | { readonly type: DefinitionKind.Label; readonly node: ts.Identifier } + | { readonly type: DefinitionKind.Keyword; readonly node: ts.Node } + | { readonly type: DefinitionKind.This; readonly node: ts.Node } + | { readonly type: DefinitionKind.String; readonly node: ts.StringLiteralLike } + | { readonly type: DefinitionKind.TripleSlashReference; readonly reference: ts.FileReference; readonly file: ts.SourceFile }; + +/** @internal */ +export const enum EntryKind { + Span, + Node, + StringLiteral, + SearchedLocalFoundProperty, + SearchedPropertyFoundLocal, +} +type NodeEntryKind = EntryKind.Node | EntryKind.StringLiteral | EntryKind.SearchedLocalFoundProperty | EntryKind.SearchedPropertyFoundLocal; +type Entry = NodeEntry | SpanEntry; +interface ContextWithStartAndEndNode { + start: ts.Node; + end: ts.Node; +} +type ContextNode = ts.Node | ContextWithStartAndEndNode; +interface NodeEntry { + readonly kind: NodeEntryKind; + readonly node: ts.Node; + readonly context?: ContextNode; +} +interface SpanEntry { + readonly kind: EntryKind.Span; + readonly fileName: string; + readonly textSpan: ts.TextSpan; +} diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 3fbded85190..6b8419291cb 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -57,7 +57,6 @@ "--vscode-chat-requestBorder", "--vscode-chat-slashCommandBackground", "--vscode-chat-slashCommandForeground", - "--vscode-chatEdits-minimapColor", "--vscode-checkbox-background", "--vscode-checkbox-border", "--vscode-checkbox-foreground", @@ -233,6 +232,8 @@ "--vscode-editorGutter-commentGlyphForeground", "--vscode-editorGutter-commentRangeForeground", "--vscode-editorGutter-commentUnresolvedGlyphForeground", + "--vscode-editorGutter-itemGlyphForeground", + "--vscode-editorGutter-itemBackground", "--vscode-editorGutter-deletedBackground", "--vscode-editorGutter-foldingControlForeground", "--vscode-editorGutter-modifiedBackground", @@ -351,6 +352,13 @@ "--vscode-extensionIcon-verifiedForeground", "--vscode-focusBorder", "--vscode-foreground", + "--vscode-gauge-background", + "--vscode-gauge-border", + "--vscode-gauge-errorBackground", + "--vscode-gauge-errorForeground", + "--vscode-gauge-foreground", + "--vscode-gauge-warningBackground", + "--vscode-gauge-warningForeground", "--vscode-icon-foreground", "--vscode-inlineChat-background", "--vscode-inlineChat-border", @@ -466,6 +474,7 @@ "--vscode-mergeEditor-conflict-unhandledUnfocused-border", "--vscode-mergeEditor-conflictingLines-background", "--vscode-minimap-background", + "--vscode-minimap-chatEditHighlight", "--vscode-minimap-errorHighlight", "--vscode-minimap-findMatchHighlight", "--vscode-minimap-foregroundOpacity", @@ -649,10 +658,6 @@ "--vscode-statusBar-noFolderForeground", "--vscode-statusBarItem-activeBackground", "--vscode-statusBarItem-compactHoverBackground", - "--vscode-statusBarItem-copilotBackground", - "--vscode-statusBarItem-copilotForeground", - "--vscode-statusBarItem-copilotHoverBackground", - "--vscode-statusBarItem-copilotHoverForeground", "--vscode-statusBarItem-errorBackground", "--vscode-statusBarItem-errorForeground", "--vscode-statusBarItem-errorHoverBackground", @@ -779,6 +784,8 @@ "--vscode-terminalStickyScroll-background", "--vscode-terminalStickyScroll-border", "--vscode-terminalStickyScrollHover-background", + "--vscode-terminalSymbolIcon-aliasForeground", + "--vscode-terminalSymbolIcon-flagForeground", "--vscode-testing-coverCountBadgeBackground", "--vscode-testing-coverCountBadgeForeground", "--vscode-testing-coveredBackground", diff --git a/build/linux/dependencies-generator.js b/build/linux/dependencies-generator.js index 39e2b4e317b..448ab38c4a4 100644 --- a/build/linux/dependencies-generator.js +++ b/build/linux/dependencies-generator.js @@ -26,7 +26,7 @@ const product = require("../../product.json"); // 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; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.196:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/dependencies-generator.ts b/build/linux/dependencies-generator.ts index 83d2ec78abb..6c1f7b7570b 100644 --- a/build/linux/dependencies-generator.ts +++ b/build/linux/dependencies-generator.ts @@ -25,7 +25,7 @@ import product = require('../../product.json'); // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.196:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js index 13f32376037..f45b6f34b84 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -46,6 +46,7 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', 'libc.so.6(GLIBC_2.25)(64bit)', + 'libc.so.6(GLIBC_2.27)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', 'libc.so.6(GLIBC_2.3.2)(64bit)', @@ -140,6 +141,7 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)', 'libc.so.6(GLIBC_2.18)', 'libc.so.6(GLIBC_2.25)', + 'libc.so.6(GLIBC_2.27)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', 'libc.so.6(GLIBC_2.6)', @@ -240,6 +242,7 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.25)(64bit)', + 'libc.so.6(GLIBC_2.27)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 63b47522d50..d277ca7e664 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -45,6 +45,7 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', 'libc.so.6(GLIBC_2.25)(64bit)', + 'libc.so.6(GLIBC_2.27)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', 'libc.so.6(GLIBC_2.3.2)(64bit)', @@ -139,6 +140,7 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)', 'libc.so.6(GLIBC_2.18)', 'libc.so.6(GLIBC_2.25)', + 'libc.so.6(GLIBC_2.27)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', 'libc.so.6(GLIBC_2.6)', @@ -239,6 +241,7 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.25)(64bit)', + 'libc.so.6(GLIBC_2.27)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index c1f22aa5002..458847afac5 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -85,7 +85,7 @@ function setNpmrcConfig(dir, env) { // the correct clang variable. So keep the version check // in preinstall sync with this logic. // Change was first introduced in https://github.com/nodejs/node/commit/6e0a2bb54c5bbeff0e9e33e1a0c683ed980a8a0f - if (dir === 'remote' && process.platform === 'darwin') { + if ((dir === 'remote' || dir === 'build') && process.platform === 'darwin') { env['npm_config_force_process_config'] = 'true'; } else { delete env['npm_config_force_process_config']; diff --git a/build/package-lock.json b/build/package-lock.json index aa939e40375..445e842c5e3 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -57,13 +57,13 @@ "source-map": "0.6.1", "ternary-stream": "^3.0.0", "through2": "^4.0.2", - "tree-sitter": "^0.20.5", + "tree-sitter": "^0.22.4", "vscode-universal-bundler": "^0.1.3", "workerpool": "^6.4.0", "yauzl": "^2.10.0" }, "optionalDependencies": { - "tree-sitter-typescript": "^0.20.5", + "tree-sitter-typescript": "^0.23.2", "vscode-gulp-watch": "^5.0.3" } }, @@ -1551,7 +1551,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -1580,7 +1580,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -1632,7 +1633,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -1647,6 +1648,7 @@ "url": "https://feross.org/support" } ], + "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -1821,7 +1823,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/clone": { "version": "2.1.2", @@ -1991,7 +1994,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "devOptional": true, + "dev": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -2006,7 +2009,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "devOptional": true, + "dev": true, "engines": { "node": ">=10" }, @@ -2018,7 +2021,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "devOptional": true, + "dev": true, + "optional": true, "engines": { "node": ">=4.0.0" } @@ -2075,7 +2079,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "devOptional": true, + "dev": true, + "optional": true, "engines": { "node": ">=8" } @@ -2214,7 +2219,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "devOptional": true, + "dev": true, "dependencies": { "once": "^1.4.0" } @@ -2331,7 +2336,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "devOptional": true, + "dev": true, + "optional": true, "engines": { "node": ">=6" } @@ -2471,7 +2477,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/fs-extra": { "version": "8.1.0", @@ -2553,7 +2560,8 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/glob": { "version": "7.2.3", @@ -2867,7 +2875,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -2881,7 +2889,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true }, "node_modules/inflight": { "version": "1.0.6", @@ -2904,7 +2913,8 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -3222,7 +3232,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "devOptional": true, + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -3330,13 +3340,14 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "devOptional": true + "dev": true }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/ms": { "version": "2.1.2", @@ -3350,23 +3361,19 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "node_modules/nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", - "devOptional": true - }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/node-abi": { "version": "3.30.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.30.0.tgz", "integrity": "sha512-qWO5l3SCqbwQavymOmtTVuCWZE23++S+rxyoHjXqUmPyzRcaoI4lA2gO55/drddGnedAyjA7sk76SfQ5lfUMnw==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "semver": "^7.3.5" }, @@ -3378,7 +3385,8 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -3402,6 +3410,18 @@ "dev": true, "optional": true }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3470,7 +3490,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E= sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, + "dev": true, "dependencies": { "wrappy": "1" } @@ -3646,7 +3666,8 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -3700,7 +3721,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "devOptional": true, + "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -3737,7 +3758,8 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -3950,27 +3972,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -3985,6 +3987,28 @@ "url": "https://feross.org/support" } ], + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -4079,7 +4103,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo= sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "devOptional": true, + "dev": true, + "optional": true, "engines": { "node": ">=0.10.0" } @@ -4119,7 +4144,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -4131,7 +4157,8 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -4211,25 +4238,86 @@ } }, "node_modules/tree-sitter": { - "version": "0.20.6", - "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.20.6.tgz", - "integrity": "sha512-GxJodajVpfgb3UREzzIbtA1hyRnTxVbWVXrbC6sk4xTMH5ERMBJk9HJNq4c8jOJeUaIOmLcwg+t6mez/PDvGqg==", - "devOptional": true, + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", + "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", + "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { - "nan": "^2.18.0", - "prebuild-install": "^7.1.1" + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + } + }, + "node_modules/tree-sitter-javascript": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/tree-sitter-javascript/-/tree-sitter-javascript-0.23.1.tgz", + "integrity": "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-javascript/node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" } }, "node_modules/tree-sitter-typescript": { - "version": "0.20.5", - "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.20.5.tgz", - "integrity": "sha512-RzK/Pc6k4GiXvInIBlo8ZggekP6rODfW2P6KHFCTSUHENsw6ynh+iacFhfkJRa4MT8EIN2WHygFJ7076/+eHKg==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.23.2.tgz", + "integrity": "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA==", "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { - "nan": "^2.18.0", - "tree-sitter": "^0.20.6" + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2", + "tree-sitter-javascript": "^0.23.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-typescript/node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/tree-sitter/node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" } }, "node_modules/tslib": { @@ -4251,7 +4339,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -4500,7 +4589,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true + "dev": true }, "node_modules/xml2js": { "version": "0.5.0", @@ -4546,7 +4635,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "devOptional": true + "dev": true }, "node_modules/yauzl": { "version": "2.10.0", diff --git a/build/package.json b/build/package.json index 3a435a8326b..73d4f42e843 100644 --- a/build/package.json +++ b/build/package.json @@ -51,7 +51,7 @@ "source-map": "0.6.1", "ternary-stream": "^3.0.0", "through2": "^4.0.2", - "tree-sitter": "^0.20.5", + "tree-sitter": "^0.22.4", "vscode-universal-bundler": "^0.1.3", "workerpool": "^6.4.0", "yauzl": "^2.10.0" @@ -63,7 +63,7 @@ "npmCheckJs": "../node_modules/.bin/tsc --noEmit" }, "optionalDependencies": { - "tree-sitter-typescript": "^0.20.5", + "tree-sitter-typescript": "^0.23.2", "vscode-gulp-watch": "^5.0.3" } } diff --git a/build/win32/Cargo.lock b/build/win32/Cargo.lock index 4c169ba0f97..5437686ef94 100644 --- a/build/win32/Cargo.lock +++ b/build/win32/Cargo.lock @@ -95,7 +95,7 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "inno_updater" -version = "0.12.1" +version = "0.13.2" dependencies = [ "byteorder", "crc", diff --git a/build/win32/Cargo.toml b/build/win32/Cargo.toml index e2130dd2bfa..42958b3124a 100644 --- a/build/win32/Cargo.toml +++ b/build/win32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "inno_updater" -version = "0.12.1" +version = "0.13.2" authors = ["Microsoft "] build = "build.rs" diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index ef7cb934eb6..2ca110dea1f 100644 Binary files a/build/win32/inno_updater.exe and b/build/win32/inno_updater.exe differ diff --git a/cgmanifest.json b/cgmanifest.json index 24c00b3731d..eb5b37d39a7 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "46a5bd1e987735e0cdc41cf48a7db4988ae73d16" + "commitHash": "5c0cb964bca15fcf41718d54f4b8d70d6b9079de" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "132.0.6834.196" + "version": "132.0.6834.210" }, { "component": { @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "53a57efd83a18efe7a84bf1b460acc789139939b" + "commitHash": "4819c99baa28bf2c1baf411ba100c467fec3d486" } }, "isOnlyProductionDependency": true, - "version": "20.18.2" + "version": "20.18.3" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "ddc7afd3f006dab49a3b6bf73bf3222b017556d5" + "commitHash": "f98501308a973e0aee2414315b426e5de2c03a60" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "34.2.0" + "version": "34.3.2" }, { "component": { diff --git a/cli/src/auth.rs b/cli/src/auth.rs index 9116e48339f..d4d62a8bf10 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -665,12 +665,12 @@ impl Auth { .into(); } - return StatusError { + StatusError { body: String::from_utf8_lossy(&body).to_string(), status_code, url: url.to_string(), } - .into(); + .into() } /// Implements the device code flow, returning the credentials upon success. async fn do_device_code_flow(&self) -> Result { diff --git a/cli/src/log.rs b/cli/src/log.rs index 538827ed124..f58f49b2176 100644 --- a/cli/src/log.rs +++ b/cli/src/log.rs @@ -282,7 +282,7 @@ pub struct DownloadLogger<'a> { logger: &'a Logger, } -impl<'a> crate::util::io::ReportCopyProgress for DownloadLogger<'a> { +impl crate::util::io::ReportCopyProgress for DownloadLogger<'_> { fn report_progress(&mut self, bytes_so_far: u64, total_bytes: u64) { if total_bytes > 0 { self.logger.emit( diff --git a/cli/src/tunnels/singleton_server.rs b/cli/src/tunnels/singleton_server.rs index 7fbd00d04d3..f183f935ce3 100644 --- a/cli/src/tunnels/singleton_server.rs +++ b/cli/src/tunnels/singleton_server.rs @@ -142,7 +142,7 @@ pub fn make_singleton_server( } } -pub async fn start_singleton_server<'a>( +pub async fn start_singleton_server( args: SingletonServerArgs<'_>, ) -> Result { let shutdown_rx = ShutdownRequest::create_rx([ diff --git a/cli/src/util/io.rs b/cli/src/util/io.rs index c5816ae55ad..4b8118a219f 100644 --- a/cli/src/util/io.rs +++ b/cli/src/util/io.rs @@ -235,6 +235,7 @@ mod tests { .write(true) .read(true) .create(true) + .truncate(true) .open(&file_path) .unwrap(); @@ -271,6 +272,7 @@ mod tests { .write(true) .read(true) .create(true) + .truncate(true) .open(&file_path) .unwrap(); @@ -305,6 +307,7 @@ mod tests { .write(true) .read(true) .create(true) + .truncate(true) .open(&file_path) .unwrap(); let mut rng = rand::thread_rng(); diff --git a/cli/src/util/prereqs.rs b/cli/src/util/prereqs.rs index f8eff34ec39..44c859772e3 100644 --- a/cli/src/util/prereqs.rs +++ b/cli/src/util/prereqs.rs @@ -20,19 +20,16 @@ lazy_static! { static ref LIBSTD_CXX_VERSION_RE: BinRegex = BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap(); static ref MIN_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 28, 0); - static ref MIN_LEGACY_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 17, 0); } #[cfg(target_arch = "arm")] lazy_static! { static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 26); - static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 22); } #[cfg(not(target_arch = "arm"))] lazy_static! { static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 25); - static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 19); } const NIXOS_TEST_PATH: &str = "/etc/NIXOS"; @@ -74,12 +71,11 @@ impl PreReqChecker { } else { println!("!!! WARNING: Skipping server pre-requisite check !!!"); println!("!!! Server stability is not guaranteed. Proceed at your own risk. !!!"); - // Use the legacy server for #210029 (Ok(true), Ok(true)) }; match (&gnu_a, &gnu_b, is_nixos) { - (Ok(false), Ok(false), _) | (_, _, true) => { + (Ok(true), Ok(true), _) | (_, _, true) => { return Ok(if cfg!(target_arch = "x86_64") { Platform::LinuxX64 } else if cfg!(target_arch = "arm") { @@ -88,15 +84,6 @@ impl PreReqChecker { Platform::LinuxARM64 }); } - (Ok(_), Ok(_), _) => { - return Ok(if cfg!(target_arch = "x86_64") { - Platform::LinuxX64Legacy - } else if cfg!(target_arch = "arm") { - Platform::LinuxARM32Legacy - } else { - Platform::LinuxARM64Legacy - }); - } _ => {} }; @@ -149,7 +136,7 @@ async fn check_musl_interpreter() -> Result<(), String> { Ok(()) } -/// Checks the glibc version, returns "true" if the legacy server is required. +/// Checks the glibc version, returns "true" if the default server is required. #[cfg(target_os = "linux")] async fn check_glibc_version() -> Result { #[cfg(target_env = "gnu")] @@ -169,8 +156,6 @@ async fn check_glibc_version() -> Result { if let Some(v) = version { return if v >= *MIN_LDD_VERSION { - Ok(false) - } else if v >= *MIN_LEGACY_LDD_VERSION { Ok(true) } else { Err(format!( @@ -192,10 +177,17 @@ async fn check_is_nixos() -> bool { /// Do not remove this check. /// Provides a way to skip the server glibc requirements check from -/// outside the install flow. A system process can create this -/// file before the server is downloaded and installed. +/// outside the install flow. +/// +/// 1) A system process can create this +/// file before the server is downloaded and installed. +/// +/// 2) An environment variable declared in host +/// that contains path to a glibc sysroot satisfying the +/// minimum requirements. #[cfg(not(windows))] pub async fn skip_requirements_check() -> bool { + std::env::var("VSCODE_SERVER_CUSTOM_GLIBC_LINKER").is_ok() || fs::metadata("/tmp/vscode-skip-server-requirements-check") .await .is_ok() @@ -206,7 +198,7 @@ pub async fn skip_requirements_check() -> bool { false } -/// Checks the glibc++ version, returns "true" if the legacy server is required. +/// Checks the glibc++ version, returns "true" if the default server is required. #[cfg(target_os = "linux")] async fn check_glibcxx_version() -> Result { let mut libstdc_path: Option = None; @@ -250,10 +242,6 @@ fn check_for_sufficient_glibcxx_versions(contents: Vec) -> Result= &*MIN_CXX_VERSION { - return Ok(false); - } - - if max_version >= &*MIN_LEGACY_CXX_VERSION { return Ok(true); } } diff --git a/eslint.config.js b/eslint.config.js index 8e3d288967b..2b005721dee 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -997,6 +997,7 @@ export default tseslint.config( { 'target': 'src/vs/workbench/api/~', 'restrictions': [ + '@c4312/eventsource-umd', 'vscode', 'vs/base/~', 'vs/base/parts/*/~', diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 224ea33c02a..531ba21fe06 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -49,6 +49,7 @@ "settings.json", "launch.json", "tasks.json", + "mcp.json", "keybindings.json", "extensions.json", "argv.json", @@ -117,6 +118,10 @@ "fileMatch": "/.vscode/tasks.json", "url": "vscode://schemas/tasks" }, + { + "fileMatch": "/.vscode/mcp.json", + "url": "vscode://schemas/mcp" + }, { "fileMatch": "%APP_SETTINGS_HOME%/tasks.json", "url": "vscode://schemas/tasks" diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index 6135df5315a..12b50f31759 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -127,7 +127,7 @@ export class SettingsDocument { completions.push(this.newSimpleCompletionItem(getText('separator'), range, vscode.l10n.t("a conditional separator (' - ') that only shows when surrounded by variables with values"))); completions.push(this.newSimpleCompletionItem(getText('activeRepositoryName'), range, vscode.l10n.t("the name of the active repository (e.g. vscode)"))); completions.push(this.newSimpleCompletionItem(getText('activeRepositoryBranchName'), range, vscode.l10n.t("the name of the active branch in the active repository (e.g. main)"))); - + completions.push(this.newSimpleCompletionItem(getText('activeEditorState'), range, vscode.l10n.t("the state of the active editor (e.g. modified)."))); return completions; } diff --git a/extensions/csharp/cgmanifest.json b/extensions/csharp/cgmanifest.json index fefe63e4786..58ae5ece50a 100644 --- a/extensions/csharp/cgmanifest.json +++ b/extensions/csharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/csharp-tmLanguage", "repositoryUrl": "https://github.com/dotnet/csharp-tmLanguage", - "commitHash": "62026a70f9fcc42d9222eccfec34ed5ee0784f3d" + "commitHash": "1381bedfb087c18aca67af8278050d11bc9d9349" } }, "license": "MIT", diff --git a/extensions/csharp/language-configuration.json b/extensions/csharp/language-configuration.json index 60814ae02f4..7db6640f4c5 100644 --- a/extensions/csharp/language-configuration.json +++ b/extensions/csharp/language-configuration.json @@ -84,9 +84,10 @@ }, "onEnterRules": [ // Add // when pressing enter from inside line comment + // We do not want to match /// (a documentation comment) { "beforeText": { - "pattern": "\/\/.*" + "pattern": "[^\/]\/\/[^\/].*" }, "afterText": { "pattern": "^(?!\\s*$).+" @@ -96,5 +97,16 @@ "appendText": "// " } }, + // Add /// when pressing enter from anywhere inside a documentation comment. + // Documentation comments are not valid after non-whitespace. + { + "beforeText": { + "pattern": "^\\s*\/\/\/" + }, + "action": { + "indent": "none", + "appendText": "/// " + } + }, ] } diff --git a/extensions/csharp/syntaxes/csharp.tmLanguage.json b/extensions/csharp/syntaxes/csharp.tmLanguage.json index c4d9a2519dc..1afcc3053b6 100644 --- a/extensions/csharp/syntaxes/csharp.tmLanguage.json +++ b/extensions/csharp/syntaxes/csharp.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/dotnet/csharp-tmLanguage/commit/62026a70f9fcc42d9222eccfec34ed5ee0784f3d", + "version": "https://github.com/dotnet/csharp-tmLanguage/commit/1381bedfb087c18aca67af8278050d11bc9d9349", "name": "C#", "scopeName": "source.cs", "patterns": [ @@ -714,11 +714,11 @@ }, "enum-declaration": { "begin": "(?=\\benum\\b)", - "end": "(?<=\\})", + "end": "(?<=\\})|(?=;)", "patterns": [ { "begin": "(?=enum)", - "end": "(?=\\{)", + "end": "(?=\\{)|(?=;)", "patterns": [ { "include": "#comment" @@ -805,7 +805,7 @@ }, "interface-declaration": { "begin": "(?=\\binterface\\b)", - "end": "(?<=\\})", + "end": "(?<=\\})|(?=;)", "patterns": [ { "begin": "(?x)\n(interface)\\b\\s+\n(@?[_[:alpha:]][_[:alnum:]]*)", @@ -817,7 +817,7 @@ "name": "entity.name.type.interface.cs" } }, - "end": "(?=\\{)", + "end": "(?=\\{)|(?=;)", "patterns": [ { "include": "#comment" diff --git a/extensions/debug-server-ready/src/extension.ts b/extensions/debug-server-ready/src/extension.ts index 22a8ff836d3..40155f306bc 100644 --- a/extensions/debug-server-ready/src/extension.ts +++ b/extensions/debug-server-ready/src/extension.ts @@ -23,7 +23,14 @@ interface ServerReadyAction { } // From src/vs/base/common/strings.ts -const CSI_SEQUENCE = /(?:(?:\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~])|(:?\x1b\].*?\x07)/g; +const CSI_SEQUENCE = /(?:\x1b\[|\x9b)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~]/; +const OSC_SEQUENCE = /(?:\x1b\]|\x9d).*?(?:\x1b\\|\x07|\x9c)/; +const ESC_SEQUENCE = /\x1b(?:[ #%\(\)\*\+\-\.\/]?[a-zA-Z0-9\|}~@])/; +const CONTROL_SEQUENCES = new RegExp('(?:' + [ + CSI_SEQUENCE.source, + OSC_SEQUENCE.source, + ESC_SEQUENCE.source, +].join('|') + ')', 'g'); /** * Froms vs/base/common/strings.ts in core @@ -31,7 +38,7 @@ const CSI_SEQUENCE = /(?:(?:\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~])| */ function removeAnsiEscapeCodes(str: string): string { if (str) { - str = str.replace(CSI_SEQUENCE, ''); + str = str.replace(CONTROL_SEQUENCES, ''); } return str; diff --git a/extensions/git/package-lock.json b/extensions/git/package-lock.json index 7a9cb2aded8..bc150555c70 100644 --- a/extensions/git/package-lock.json +++ b/extensions/git/package-lock.json @@ -14,6 +14,7 @@ "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", + "jschardet": "3.1.4", "picomatch": "2.3.1", "vscode-uri": "^2.0.0", "which": "4.0.0" @@ -278,6 +279,15 @@ "node": ">=16" } }, + "node_modules/jschardet": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz", + "integrity": "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==", + "license": "LGPL-2.1+", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/peek-readable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", diff --git a/extensions/git/package.json b/extensions/git/package.json index 06f40d948a1..7d89571372b 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -21,7 +21,6 @@ "contribSourceControlInputBoxMenu", "contribSourceControlTitleMenu", "contribViewsWelcome", - "diffCommand", "editSessionIdentityProvider", "quickDiffProvider", "quickInputButtonLocation", @@ -35,7 +34,6 @@ "statusBarItemTooltip", "tabInputMultiDiff", "tabInputTextMerge", - "textDocumentEncoding", "textEditorDiffInformation", "timeline" ], @@ -3323,10 +3321,15 @@ "markdownDescription": "%config.diagnosticsCommitHook.Sources%", "scope": "resource" }, - "git.untrackedChangesSoftDelete": { + "git.discardUntrackedChangesToTrash": { "type": "boolean", "default": true, - "markdownDescription": "%config.untrackedChangesSoftDelete%" + "markdownDescription": "%config.discardUntrackedChangesToTrash%" + }, + "git.showReferenceDetails": { + "type": "boolean", + "default": false, + "markdownDescription": "%config.showReferenceDetails%" } } }, @@ -3435,10 +3438,10 @@ "id": "git.blame.editorDecorationForeground", "description": "%colors.blameEditorDecoration%", "defaults": { - "dark": "editorCodeLens.foreground", - "light": "editorCodeLens.foreground", - "highContrast": "editorCodeLens.foreground", - "highContrastLight": "editorCodeLens.foreground" + "dark": "editorInlayHint.foreground", + "light": "editorInlayHint.foreground", + "highContrast": "editorInlayHint.foreground", + "highContrastLight": "editorInlayHint.foreground" } } ], @@ -3567,6 +3570,7 @@ "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", + "jschardet": "3.1.4", "picomatch": "2.3.1", "vscode-uri": "^2.0.0", "which": "4.0.0" diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 08b0cc49ed5..52ba3819817 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -288,7 +288,8 @@ "config.commitShortHashLength": "Controls the length of the commit short hash.", "config.diagnosticsCommitHook.Enabled": "Controls whether to check for unresolved diagnostics before committing.", "config.diagnosticsCommitHook.Sources": "Controls the list of sources (**Item**) and the minimum severity (**Value**) to be considered before committing. **Note:** To ignore diagnostics from a particular source, add the source to the list and set the minimum severity to `none`.", - "config.untrackedChangesSoftDelete": "Controls whether discarding untracked changes moves the file(s) to the Recycle Bin (Windows), Trash (macOS, Linux) instead of deleting them. **Note:** This setting has no effect when connected to a remote.", + "config.discardUntrackedChangesToTrash": "Controls whether discarding untracked changes moves the file(s) to the Recycle Bin (Windows), Trash (macOS, Linux) instead of deleting them permanently. **Note:** This setting has no effect when connected to a remote or when running in Linux as a snap package.", + "config.showReferenceDetails": "Controls whether to show the details of the last commit for Git refs in the checkout, branch, and tag pickers.", "submenu.explorer": "Git", "submenu.commit": "Commit", "submenu.commit.amend": "Amend", diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 5bbb4b03a9c..9562106f9c6 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -30,6 +30,7 @@ export interface Ref { readonly type: RefType; readonly name?: string; readonly commit?: string; + readonly commitDetails?: Commit; readonly remote?: string; } @@ -185,6 +186,7 @@ export interface RefQuery { readonly contains?: string; readonly count?: number; readonly pattern?: string | string[]; + readonly includeCommitDetails?: boolean; readonly sort?: 'alphabetically' | 'committerdate'; } diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index f29212c51a8..12182f1de8d 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -69,6 +69,41 @@ function isResourceSchemeSupported(uri: Uri): boolean { return uri.scheme === 'file' || isGitUri(uri); } +function isResourceBlameInformationEqual(a: ResourceBlameInformation | undefined, b: ResourceBlameInformation | undefined): boolean { + if (a === b) { + return true; + } + + if (!a || !b || + a.resource.toString() !== b.resource.toString() || + a.blameInformation.length !== b.blameInformation.length) { + return false; + } + + for (let index = 0; index < a.blameInformation.length; index++) { + if (a.blameInformation[index].lineNumber !== b.blameInformation[index].lineNumber) { + return false; + } + + const aBlameInformation = a.blameInformation[index].blameInformation; + const bBlameInformation = b.blameInformation[index].blameInformation; + + if (typeof aBlameInformation === 'string' && typeof bBlameInformation === 'string') { + if (aBlameInformation !== bBlameInformation) { + return false; + } + } else if (typeof aBlameInformation !== 'string' && typeof bBlameInformation !== 'string') { + if (aBlameInformation.hash !== bBlameInformation.hash) { + return false; + } + } else { + return false; + } + } + + return true; +} + type BlameInformationTemplateTokens = { readonly hash: string; readonly hashShort: string; @@ -79,6 +114,11 @@ type BlameInformationTemplateTokens = { readonly authorDateAgo: string; }; +interface ResourceBlameInformation { + readonly resource: Uri; + readonly blameInformation: readonly LineBlameInformation[]; +} + interface LineBlameInformation { readonly lineNumber: number; readonly blameInformation: BlameInformation | string; @@ -116,11 +156,15 @@ export class GitBlameController { private readonly _onDidChangeBlameInformation = new EventEmitter(); public readonly onDidChangeBlameInformation = this._onDidChangeBlameInformation.event; - private _textEditorBlameInformation: LineBlameInformation[] | undefined; - get textEditorBlameInformation(): readonly LineBlameInformation[] | undefined { + private _textEditorBlameInformation: ResourceBlameInformation | undefined; + get textEditorBlameInformation(): ResourceBlameInformation | undefined { return this._textEditorBlameInformation; } - private set textEditorBlameInformation(blameInformation: LineBlameInformation[] | undefined) { + private set textEditorBlameInformation(blameInformation: ResourceBlameInformation | undefined) { + if (isResourceBlameInformationEqual(this._textEditorBlameInformation, blameInformation)) { + return; + } + this._textEditorBlameInformation = blameInformation; this._onDidChangeBlameInformation.fire(); } @@ -318,7 +362,7 @@ export class GitBlameController { } window.onDidChangeActiveTextEditor(e => this._updateTextEditorBlameInformation(e), this, this._enablementDisposables); - window.onDidChangeTextEditorSelection(e => this._updateTextEditorBlameInformation(e.textEditor, true), this, this._enablementDisposables); + window.onDidChangeTextEditorSelection(e => this._updateTextEditorBlameInformation(e.textEditor, 'selection'), this, this._enablementDisposables); window.onDidChangeTextEditorDiffInformation(e => this._updateTextEditorBlameInformation(e.textEditor), this, this._enablementDisposables); } } else { @@ -377,7 +421,7 @@ export class GitBlameController { } @throttle - private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined, showBlameInformationForPositionZero = false): Promise { + private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined, reason?: 'selection'): Promise { if (textEditor) { if (!textEditor.diffInformation || textEditor !== window.activeTextEditor) { return; @@ -401,7 +445,7 @@ export class GitBlameController { // Do not show blame information when there is a single selection and it is at the beginning // of the file [0, 0, 0, 0] unless the user explicitly navigates the cursor there. We do this // to avoid showing blame information when the editor is not focused. - if (!showBlameInformationForPositionZero && textEditor.selections.length === 1 && + if (reason !== 'selection' && textEditor.selections.length === 1 && textEditor.selections[0].start.line === 0 && textEditor.selections[0].start.character === 0 && textEditor.selections[0].end.line === 0 && textEditor.selections[0].end.character === 0) { this.textEditorBlameInformation = undefined; @@ -426,8 +470,11 @@ export class GitBlameController { // Resource on the right-hand side of the diff editor when viewing a resource from the index. const diffInformationWorkingTreeAndIndex = getWorkingTreeAndIndexDiffInformation(textEditor); - // Working tree + index diff information is present and it is stale + // Working tree + index diff information is present and it is stale. Diff information + // may be stale when the selection changes because of a content change and the diff + // information is not yet updated. if (diffInformationWorkingTreeAndIndex && diffInformationWorkingTreeAndIndex.isStale) { + this.textEditorBlameInformation = undefined; return; } @@ -440,16 +487,22 @@ export class GitBlameController { // Working tree diff information. Diff Editor (Working Tree) -> Text Editor const diffInformationWorkingTree = getWorkingTreeDiffInformation(textEditor); - // Working tree diff information is not present or it is stale + // Working tree diff information is not present or it is stale. Diff information + // may be stale when the selection changes because of a content change and the diff + // information is not yet updated. if (!diffInformationWorkingTree || diffInformationWorkingTree.isStale) { + this.textEditorBlameInformation = undefined; return; } // Working tree + index diff information const diffInformationWorkingTreeAndIndex = getWorkingTreeAndIndexDiffInformation(textEditor); - // Working tree + index diff information is present and it is stale + // Working tree + index diff information is present and it is stale. Diff information + // may be stale when the selection changes because of a content change and the diff + // information is not yet updated. if (diffInformationWorkingTreeAndIndex && diffInformationWorkingTreeAndIndex.isStale) { + this.textEditorBlameInformation = undefined; return; } @@ -482,7 +535,10 @@ export class GitBlameController { for (const lineNumber of new Set(textEditor.selections.map(s => s.active.line))) { // Check if the line is contained in the working tree diff information if (lineRangesContainLine(workingTreeChanges, lineNumber + 1)) { - lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet') }); + if (reason === 'selection') { + // Only show the `Not Committed Yet` message upon selection change due to navigation + lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet') }); + } continue; } @@ -505,7 +561,10 @@ export class GitBlameController { } } - this.textEditorBlameInformation = lineBlameInformation; + this.textEditorBlameInformation = { + resource: textEditor.document.uri, + blameInformation: lineBlameInformation + }; } dispose() { @@ -555,7 +614,7 @@ class GitBlameEditorDecoration implements HoverProvider { } // Get blame information - const blameInformation = this._controller.textEditorBlameInformation; + const blameInformation = this._controller.textEditorBlameInformation?.blameInformation; const lineBlameInformation = blameInformation?.find(blame => blame.lineNumber === position.line); if (!lineBlameInformation || typeof lineBlameInformation.blameInformation === 'string') { @@ -601,8 +660,8 @@ class GitBlameEditorDecoration implements HoverProvider { } // Get blame information - const blameInformation = this._controller.textEditorBlameInformation; - if (!blameInformation) { + const blameInformation = this._controller.textEditorBlameInformation?.blameInformation; + if (!blameInformation || blameInformation.length === 0) { textEditor.setDecorations(this._decoration, []); return; } @@ -680,7 +739,7 @@ class GitBlameStatusBarItem { return; } - const blameInformation = this._controller.textEditorBlameInformation; + const blameInformation = this._controller.textEditorBlameInformation?.blameInformation; if (!blameInformation || blameInformation.length === 0) { this._statusBarItem.hide(); return; diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index b09e6d5a6c5..1094a438b07 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,16 +5,16 @@ import * as os from 'os'; import * as path from 'path'; -import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages } from 'vscode'; +import { Command, commands, Disposable, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import { ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote } from './api/git'; import { Git, Stash } from './git'; import { Model } from './model'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; -import { DiffEditorSelectionHunkToolbarContext, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; +import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; -import { DiagnosticSeverityConfig, dispose, getCommitShortHash, grep, isDefined, isDescendant, isRemote, isWindows, pathEquals, relativePath, toDiagnosticSeverity, truncate } from './util'; +import { DiagnosticSeverityConfig, dispose, fromNow, getCommitShortHash, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, toDiagnosticSeverity, truncate } from './util'; import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; import { getRemoteSourceActions, pickRemoteSource } from './remoteSource'; @@ -73,6 +73,10 @@ class RefItem implements QuickPickItem { } get description(): string { + if (this.ref.commitDetails?.authorDate) { + return fromNow(this.ref.commitDetails.authorDate, true, true); + } + switch (this.ref.type) { case RefType.Head: return this.shortCommit; @@ -85,6 +89,14 @@ class RefItem implements QuickPickItem { } } + get detail(): string | undefined { + if (this.ref.commitDetails?.authorName && this.ref.commitDetails?.message) { + return `${this.ref.commitDetails?.authorName} | ${this.ref.commitDetails?.message}`; + } + + return undefined; + } + get refName(): string | undefined { return this.ref.name; } get refRemote(): string | undefined { return this.ref.remote; } get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); } @@ -275,7 +287,6 @@ class StashItem implements QuickPickItem { interface ScmCommandOptions { repository?: boolean; - diff?: boolean; } interface ScmCommand { @@ -327,6 +338,8 @@ async function categorizeResourceByResolution(resources: Resource[]): Promise<{ async function createCheckoutItems(repository: Repository, detached = false): Promise { const config = workspace.getConfiguration('git'); const checkoutTypeConfig = config.get('checkoutType'); + const showRefDetails = config.get('showReferenceDetails') === true; + let checkoutTypes: string[]; if (checkoutTypeConfig === 'all' || !checkoutTypeConfig || checkoutTypeConfig.length === 0) { @@ -342,7 +355,7 @@ async function createCheckoutItems(repository: Repository, detached = false): Pr checkoutTypes = checkoutTypes.filter(t => t !== 'tags'); } - const refs = await repository.getRefs(); + const refs = await repository.getRefs({ includeCommitDetails: showRefDetails }); const refProcessors = checkoutTypes.map(type => getCheckoutRefProcessor(repository, type)) .filter(p => !!p) as RefProcessor[]; @@ -712,12 +725,7 @@ export class CommandCenter { ) { this.disposables = Commands.map(({ commandId, key, method, options }) => { const command = this.createCommand(commandId, key, method, options); - - if (options.diff) { - return commands.registerDiffInformationCommand(commandId, command); - } else { - return commands.registerCommand(commandId, command); - } + return commands.registerCommand(commandId, command); }); this.disposables.push(workspace.registerTextDocumentContentProvider('git-output', this.commandErrors)); @@ -1628,33 +1636,23 @@ export class CommandCenter { } let modifiedUri = changes.modifiedUri; - let modifiedDocument: TextDocument | undefined; - if (!modifiedUri) { const textEditor = window.activeTextEditor; if (!textEditor) { return; } - - modifiedDocument = textEditor.document; + const modifiedDocument = textEditor.document; modifiedUri = modifiedDocument.uri; } - if (modifiedUri.scheme !== 'file') { return; } - - if (!modifiedDocument) { - modifiedDocument = await workspace.openTextDocument(modifiedUri); - } - const result = changes.originalWithModifiedChanges; - await this.runByRepository(modifiedUri, async (repository, resource) => - await repository.stage(resource, result, modifiedDocument.encoding)); + await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result)); } - @command('git.stageSelectedRanges', { diff: true }) - async stageSelectedChanges(changes: LineChange[]): Promise { + @command('git.stageSelectedRanges') + async stageSelectedChanges(): Promise { const textEditor = window.activeTextEditor; if (!textEditor) { @@ -1668,7 +1666,6 @@ export class CommandCenter { const workingTreeLineChanges = toLineChanges(workingTreeDiffInformation); - this.logger.trace(`[CommandCenter][stageSelectedChanges] changes: ${JSON.stringify(changes)}`); this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`); this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation changes: ${JSON.stringify(workingTreeLineChanges)}`); @@ -1827,8 +1824,7 @@ export class CommandCenter { const originalDocument = await workspace.openTextDocument(originalUri); const result = applyLineChanges(originalDocument, modifiedDocument, changes); - await this.runByRepository(modifiedUri, async (repository, resource) => - await repository.stage(resource, result, modifiedDocument.encoding)); + await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result)); } @command('git.revertChange') @@ -1849,8 +1845,8 @@ export class CommandCenter { textEditor.selections = [new Selection(firstStagedLine, 0, firstStagedLine, 0)]; } - @command('git.revertSelectedRanges', { diff: true }) - async revertSelectedRanges(changes: LineChange[]): Promise { + @command('git.revertSelectedRanges') + async revertSelectedRanges(): Promise { const textEditor = window.activeTextEditor; if (!textEditor) { @@ -1864,7 +1860,6 @@ export class CommandCenter { const workingTreeLineChanges = toLineChanges(workingTreeDiffInformation); - this.logger.trace(`[CommandCenter][revertSelectedRanges] changes: ${JSON.stringify(changes)}`); this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`); this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation changes: ${JSON.stringify(workingTreeLineChanges)}`); @@ -1939,8 +1934,8 @@ export class CommandCenter { await repository.revert([]); } - @command('git.unstageSelectedRanges', { diff: true }) - async unstageSelectedRanges(changes: LineChange[]): Promise { + @command('git.unstageSelectedRanges') + async unstageSelectedRanges(): Promise { const textEditor = window.activeTextEditor; if (!textEditor) { @@ -1978,7 +1973,6 @@ export class CommandCenter { const indexLineChanges = toLineChanges(indexDiffInformation); - this.logger.trace(`[CommandCenter][unstageSelectedRanges] changes: ${JSON.stringify(changes)}`); this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation: ${JSON.stringify(indexDiffInformation)}`); this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation changes: ${JSON.stringify(indexLineChanges)}`); @@ -2000,7 +1994,7 @@ export class CommandCenter { this.logger.trace(`[CommandCenter][unstageSelectedRanges] invertedDiffs: ${JSON.stringify(invertedDiffs)}`); const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs); - await repository.stage(modifiedUri, result, modifiedDocument.encoding); + await repository.stage(modifiedUri, result); } @command('git.unstageFile') @@ -2175,9 +2169,9 @@ export class CommandCenter { private getDiscardUntrackedChangesDialogDetails(resources: Resource[]): [string, string, string] { const config = workspace.getConfiguration('git'); - const untrackedChangesSoftDelete = config.get('untrackedChangesSoftDelete', true) && !isRemote; + const discardUntrackedChangesToTrash = config.get('discardUntrackedChangesToTrash', true) && !isRemote && !isLinuxSnap; - const messageWarning = !untrackedChangesSoftDelete + const messageWarning = !discardUntrackedChangesToTrash ? resources.length === 1 ? '\n\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST if you proceed.' : '\n\nThis is IRREVERSIBLE!\nThese files will be FOREVER LOST if you proceed.' @@ -2187,7 +2181,7 @@ export class CommandCenter { ? l10n.t('Are you sure you want to DELETE the following untracked file: \'{0}\'?{1}', path.basename(resources[0].resourceUri.fsPath), messageWarning) : l10n.t('Are you sure you want to DELETE the {0} untracked files?{1}', resources.length, messageWarning); - const messageDetail = untrackedChangesSoftDelete + const messageDetail = discardUntrackedChangesToTrash ? isWindows ? resources.length === 1 ? 'You can restore this file from the Recycle Bin.' @@ -2197,7 +2191,7 @@ export class CommandCenter { : 'You can restore these files from the Trash.' : ''; - const primaryAction = untrackedChangesSoftDelete + const primaryAction = discardUntrackedChangesToTrash ? isWindows ? l10n.t('Move to Recycle Bin') : l10n.t('Move to Trash') @@ -2716,6 +2710,7 @@ export class CommandCenter { const quickPick = window.createQuickPick(); quickPick.busy = true; quickPick.sortByLabel = false; + quickPick.matchOnDetail = true; quickPick.placeholder = opts?.detached ? l10n.t('Select a branch to checkout in detached mode') : l10n.t('Select a branch or tag to checkout'); @@ -2936,9 +2931,12 @@ export class CommandCenter { private async _branch(repository: Repository, defaultName?: string, from = false, target?: string): Promise { target = target ?? 'HEAD'; + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; + if (from) { const getRefPicks = async () => { - const refs = await repository.getRefs(); + const refs = await repository.getRefs({ includeCommitDetails: showRefDetails }); const refProcessors = new RefItemsProcessor([ new RefProcessor(RefType.Head), new RefProcessor(RefType.RemoteHead), @@ -3041,6 +3039,9 @@ export class CommandCenter { private async _deleteBranch(repository: Repository, remote: string | undefined, name: string | undefined, options: { remote: boolean; force?: boolean }): Promise { let run: (force?: boolean) => Promise; + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; + if (!options.remote && typeof name === 'string') { // Local branch run = force => repository.deleteBranch(name!, force); @@ -3050,7 +3051,7 @@ export class CommandCenter { } else { const getBranchPicks = async () => { const pattern = options.remote ? 'refs/remotes' : 'refs/heads'; - const refs = await repository.getRefs({ pattern }); + const refs = await repository.getRefs({ pattern, includeCommitDetails: showRefDetails }); const refsToExclude: string[] = []; if (options.remote) { @@ -3128,8 +3129,11 @@ export class CommandCenter { @command('git.merge', { repository: true }) async merge(repository: Repository): Promise { + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; + const getQuickPickItems = async (): Promise => { - const refs = await repository.getRefs(); + const refs = await repository.getRefs({ includeCommitDetails: showRefDetails }); const itemsProcessor = new RefItemsProcessor([ new RefProcessor(RefType.Head, MergeItem), new RefProcessor(RefType.RemoteHead, MergeItem), @@ -3154,8 +3158,11 @@ export class CommandCenter { @command('git.rebase', { repository: true }) async rebase(repository: Repository): Promise { + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; + const getQuickPickItems = async (): Promise => { - const refs = await repository.getRefs(); + const refs = await repository.getRefs({ includeCommitDetails: showRefDetails }); const itemsProcessor = new RebaseItemsProcessors(repository); return itemsProcessor.processRefs(refs); @@ -3193,8 +3200,11 @@ export class CommandCenter { @command('git.deleteTag', { repository: true }) async deleteTag(repository: Repository): Promise { + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; + const tagPicks = async (): Promise => { - const remoteTags = await repository.getRefs({ pattern: 'refs/tags' }); + const remoteTags = await repository.getRefs({ pattern: 'refs/tags', includeCommitDetails: showRefDetails }); return remoteTags.length === 0 ? [{ label: l10n.t('$(info) This repository has no tags.') }] : remoteTags.map(ref => new TagDeleteItem(ref)); }; diff --git a/extensions/git/src/decorators.ts b/extensions/git/src/decorators.ts index b1a25d4fd91..f89ff2327e9 100644 --- a/extensions/git/src/decorators.ts +++ b/extensions/git/src/decorators.ts @@ -98,4 +98,4 @@ export function debounce(delay: number): Function { this[timerKey] = setTimeout(() => fn.apply(this, args), delay); }; }); -} \ No newline at end of file +} diff --git a/extensions/git/src/encoding.ts b/extensions/git/src/encoding.ts new file mode 100644 index 00000000000..c80fb6ee6d5 --- /dev/null +++ b/extensions/git/src/encoding.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as jschardet from 'jschardet'; + +function detectEncodingByBOM(buffer: Buffer): string | null { + if (!buffer || buffer.length < 2) { + return null; + } + + const b0 = buffer.readUInt8(0); + const b1 = buffer.readUInt8(1); + + // UTF-16 BE + if (b0 === 0xFE && b1 === 0xFF) { + return 'utf16be'; + } + + // UTF-16 LE + if (b0 === 0xFF && b1 === 0xFE) { + return 'utf16le'; + } + + if (buffer.length < 3) { + return null; + } + + const b2 = buffer.readUInt8(2); + + // UTF-8 + if (b0 === 0xEF && b1 === 0xBB && b2 === 0xBF) { + return 'utf8'; + } + + return null; +} + +const IGNORE_ENCODINGS = [ + 'ascii', + 'utf-8', + 'utf-16', + 'utf-32' +]; + +const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { + 'ibm866': 'cp866', + 'big5': 'cp950' +}; + +const MAP_CANDIDATE_GUESS_ENCODING_TO_JSCHARDET: { [key: string]: string } = { + utf8: 'UTF-8', + utf16le: 'UTF-16LE', + utf16be: 'UTF-16BE', + windows1252: 'windows-1252', + windows1250: 'windows-1250', + iso88592: 'ISO-8859-2', + windows1251: 'windows-1251', + cp866: 'IBM866', + iso88595: 'ISO-8859-5', + koi8r: 'KOI8-R', + windows1253: 'windows-1253', + iso88597: 'ISO-8859-7', + windows1255: 'windows-1255', + iso88598: 'ISO-8859-8', + cp950: 'Big5', + shiftjis: 'SHIFT_JIS', + eucjp: 'EUC-JP', + euckr: 'EUC-KR', + gb2312: 'GB2312' +}; + +export function detectEncoding(buffer: Buffer, candidateGuessEncodings: string[]): string | null { + const result = detectEncodingByBOM(buffer); + + if (result) { + return result; + } + + candidateGuessEncodings = candidateGuessEncodings.map(e => MAP_CANDIDATE_GUESS_ENCODING_TO_JSCHARDET[e]).filter(e => !!e); + + const detected = jschardet.detect(buffer, candidateGuessEncodings.length > 0 ? { detectEncodings: candidateGuessEncodings } : undefined); + if (!detected || !detected.encoding) { + return null; + } + + const encoding = detected.encoding; + + // Ignore encodings that cannot guess correctly + // (http://chardet.readthedocs.io/en/latest/supported-encodings.html) + if (0 <= IGNORE_ENCODINGS.indexOf(encoding.toLowerCase())) { + return null; + } + + const normalizedEncodingName = encoding.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); + const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName]; + + return mapped || normalizedEncodingName; +} diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 29fc38abfed..4f64607aa6a 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -12,9 +12,10 @@ import which from 'which'; import { EventEmitter } from 'events'; import * as iconv from '@vscode/iconv-lite-umd'; import * as filetype from 'file-type'; -import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant } from './util'; +import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant, relativePath } from './util'; import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode'; -import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery, InitOptions } from './api/git'; +import { detectEncoding } from './encoding'; +import { Commit as ApiCommit, Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery, InitOptions } from './api/git'; import * as byline from 'byline'; import { StringDecoder } from 'string_decoder'; @@ -354,6 +355,10 @@ function sanitizePath(path: string): string { return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`); } +function sanitizeRelativePath(from: string, to: string): string { + return path.isAbsolute(to) ? relativePath(from, to).replace(/\\/g, '/') : to; +} + const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B'; const STASH_FORMAT = '%H%n%P%n%gd%n%gs'; @@ -1125,6 +1130,70 @@ function parseGitBlame(data: string): BlameInformation[] { return Array.from(blameInformation.values()); } +const REFS_FORMAT = '%(refname)%00%(objectname)%00%(*objectname)'; +const REFS_WITH_DETAILS_FORMAT = `${REFS_FORMAT}%00%(parent)%00%(*parent)%00%(authorname)%00%(*authorname)%00%(authordate:unix)%00%(*authordate:unix)%00%(subject)%00%(*subject)`; + +const headRegex = /^refs\/heads\/([^ ]+)$/; +const remoteHeadRegex = /^refs\/remotes\/([^/]+)\/([^ ]+)$/; +const tagRegex = /^refs\/tags\/([^ ]+)$/; + +function parseRefs(data: string, includeCommitDetails: boolean): Ref[] { + const refs: Ref[] = []; + const refRegex = !includeCommitDetails + ? /^(.*)\0([0-9a-f]{40})\0([0-9a-f]{40})?$/gm + : /^(.*)\0([0-9a-f]{40})\0([0-9a-f]{40})?\0(.*)\0(.*)\0(.*)\0(.*)\0(.*)\0(.*)\0(.*)\0(.*)$/gm; + + let ref: string | undefined; + let commitHash: string | undefined; + let tagCommitHash: string | undefined; + let commitParents: string | undefined; + let tagCommitParents: string | undefined; + let commitSubject: string | undefined; + let tagCommitSubject: string | undefined; + let authorName: string | undefined; + let tagAuthorName: string | undefined; + let authorDate: string | undefined; + let tagAuthorDate: string | undefined; + + let match: RegExpExecArray | null; + let refMatch: RegExpExecArray | null; + + do { + match = refRegex.exec(data); + if (match === null) { + break; + } + + let commitDetails: ApiCommit | undefined = undefined; + [, ref, commitHash, tagCommitHash, commitParents, tagCommitParents, authorName, tagAuthorName, authorDate, tagAuthorDate, commitSubject, tagCommitSubject] = match; + + if (includeCommitDetails) { + const parents = tagCommitParents || commitParents; + const subject = tagCommitSubject || commitSubject; + const author = tagAuthorName || authorName; + const date = tagAuthorDate || authorDate; + + commitDetails = { + hash: commitHash, + message: subject, + parents: parents ? parents.split(' ') : [], + authorName: author, + authorDate: date ? new Date(Number(date) * 1000) : undefined + } satisfies ApiCommit; + } + + if (refMatch = headRegex.exec(ref)) { + refs.push({ name: refMatch[1], commit: commitHash, commitDetails, type: RefType.Head }); + } else if (refMatch = remoteHeadRegex.exec(ref)) { + refs.push({ name: `${refMatch[1]}/${refMatch[2]}`, remote: refMatch[1], commit: commitHash, commitDetails, type: RefType.RemoteHead }); + } else if (refMatch = tagRegex.exec(ref)) { + refs.push({ name: refMatch[1], commit: tagCommitHash ?? commitHash, commitDetails, type: RefType.Tag }); + } + } while (true); + + return refs; +} + export interface PullOptions { readonly unshallow?: boolean; readonly tags?: boolean; @@ -1329,8 +1398,21 @@ export class Repository { .filter(entry => !!entry); } - async buffer(object: string): Promise { - const child = this.stream(['show', '--textconv', object]); + async bufferString(ref: string, filePath: string, encoding: string = 'utf8', autoGuessEncoding = false, candidateGuessEncodings: string[] = []): Promise { + const stdout = await this.buffer(ref, filePath); + + if (autoGuessEncoding) { + encoding = detectEncoding(stdout, candidateGuessEncodings) || encoding; + } + + encoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; + + return iconv.decode(stdout, encoding); + } + + async buffer(ref: string, filePath: string): Promise { + const relativePath = sanitizeRelativePath(this.repositoryRoot, filePath); + const child = this.stream(['show', '--textconv', `${ref}:${relativePath}`]); if (!child.stdout) { return Promise.reject('Can\'t open file from git'); @@ -1382,10 +1464,17 @@ export class Repository { return { mode, object, size: parseInt(size) || 0 }; } - async lstree(treeish: string, path?: string): Promise { - const args = ['ls-tree', '-l', treeish]; + async lstree(treeish: string, path?: string, options?: { recursive?: boolean }): Promise { + const args = ['ls-tree', '-l']; + + if (options?.recursive) { + args.push('-r'); + } + + args.push(treeish); + if (path) { - args.push('--', sanitizePath(path)); + args.push('--', sanitizeRelativePath(this.repositoryRoot, path)); } const { stdout } = await this.exec(args); @@ -1393,15 +1482,24 @@ export class Repository { } async lsfiles(path: string): Promise { - const { stdout } = await this.exec(['ls-files', '--stage', '--', sanitizePath(path)]); + const args = ['ls-files', '--stage']; + const relativePath = sanitizeRelativePath(this.repositoryRoot, path); + + if (relativePath) { + args.push('--', relativePath); + } + + const { stdout } = await this.exec(args); return parseLsFiles(stdout); } - async getGitRelativePath(ref: string, relativePath: string): Promise { - const relativePathLowercase = relativePath.toLowerCase(); - const dirname = path.posix.dirname(relativePath) + '/'; - const elements: { file: string }[] = ref ? await this.lstree(ref, dirname) : await this.lsfiles(dirname); - const element = elements.filter(file => file.file.toLowerCase() === relativePathLowercase)[0]; + async getGitFilePath(ref: string, filePath: string): Promise { + const elements: { file: string }[] = ref + ? await this.lstree(ref, undefined, { recursive: true }) + : await this.lsfiles(this.repositoryRoot); + + const relativePathLowercase = sanitizeRelativePath(this.repositoryRoot, filePath).toLowerCase(); + const element = elements.find(file => file.file.toLowerCase() === relativePathLowercase); if (!element) { throw new GitError({ @@ -1409,7 +1507,7 @@ export class Repository { }); } - return element.file; + return path.join(this.repositoryRoot, element.file); } async detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> { @@ -1489,7 +1587,7 @@ export class Repository { return await this.diffFiles(false); } - const args = ['diff', '--', sanitizePath(path)]; + const args = ['diff', '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout; } @@ -1502,7 +1600,7 @@ export class Repository { return await this.diffFiles(false, ref); } - const args = ['diff', ref, '--', sanitizePath(path)]; + const args = ['diff', ref, '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout; } @@ -1515,7 +1613,7 @@ export class Repository { return await this.diffFiles(true); } - const args = ['diff', '--cached', '--', sanitizePath(path)]; + const args = ['diff', '--cached', '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout; } @@ -1528,7 +1626,7 @@ export class Repository { return await this.diffFiles(true, ref); } - const args = ['diff', '--cached', ref, '--', sanitizePath(path)]; + const args = ['diff', '--cached', ref, '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout; } @@ -1548,7 +1646,7 @@ export class Repository { return await this.diffFiles(false, range); } - const args = ['diff', range, '--', sanitizePath(path)]; + const args = ['diff', range, '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout.trim(); @@ -1640,7 +1738,7 @@ export class Repository { } if (paths && paths.length) { - for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } else { @@ -1655,13 +1753,14 @@ export class Repository { return; } - args.push(...paths.map(sanitizePath)); + args.push(...paths.map(p => sanitizeRelativePath(this.repositoryRoot, p))); await this.exec(args); } async stage(path: string, data: string, encoding: string): Promise { - const child = this.stream(['hash-object', '--stdin', '-w', '--path', sanitizePath(path)], { stdio: [null, null, null] }); + const relativePath = sanitizeRelativePath(this.repositoryRoot, path); + const child = this.stream(['hash-object', '--stdin', '-w', '--path', relativePath], { stdio: [null, null, null] }); child.stdin!.end(iconv.encode(data, encoding)); const { exitCode, stdout } = await exec(child); @@ -1690,7 +1789,7 @@ export class Repository { add = '--add'; } - await this.exec(['update-index', add, '--cacheinfo', mode, hash, path]); + await this.exec(['update-index', add, '--cacheinfo', mode, hash, relativePath]); } async checkout(treeish: string, paths: string[], opts: { track?: boolean; detached?: boolean } = Object.create(null)): Promise { @@ -1710,7 +1809,7 @@ export class Repository { try { if (paths && paths.length > 0) { - for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } else { @@ -1924,7 +2023,7 @@ export class Repository { const args = ['clean', '-f', '-q']; for (const paths of groups) { - for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { promises.push(limiter.queue(() => this.exec([...args, '--', ...chunk]))); } } @@ -1964,7 +2063,7 @@ export class Repository { try { if (paths && paths.length > 0) { - for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } else { @@ -2209,7 +2308,7 @@ export class Repository { async blame(path: string): Promise { try { - const args = ['blame', sanitizePath(path)]; + const args = ['blame', '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout.trim(); } catch (err) { @@ -2229,7 +2328,7 @@ export class Repository { args.push(ref); } - args.push('--', sanitizePath(path)); + args.push('--', sanitizeRelativePath(this.repositoryRoot, path)); const result = await this.exec(args); @@ -2550,7 +2649,7 @@ export class Repository { args.push('--sort', `-${query.sort}`); } - args.push('--format', '%(refname) %(objectname) %(*objectname)'); + args.push('--format', query.includeCommitDetails ? REFS_WITH_DETAILS_FORMAT : REFS_FORMAT); if (query.pattern) { const patterns = Array.isArray(query.pattern) ? query.pattern : [query.pattern]; @@ -2564,25 +2663,7 @@ export class Repository { } const result = await this.exec(args, { cancellationToken }); - - const fn = (line: string): Ref | null => { - let match: RegExpExecArray | null; - - if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) { - return { name: match[1], commit: match[2], type: RefType.Head }; - } else if (match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) { - return { name: `${match[1]}/${match[2]}`, commit: match[3], type: RefType.RemoteHead, remote: match[1] }; - } else if (match = /^refs\/tags\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) { - return { name: match[1], commit: match[3] ?? match[2], type: RefType.Tag }; - } - - return null; - }; - - return result.stdout.split('\n') - .filter(line => !!line) - .map(fn) - .filter(ref => !!ref) as Ref[]; + return parseRefs(result.stdout, query.includeCommitDetails === true); } async getRemoteRefs(remote: string, opts?: { heads?: boolean; tags?: boolean; cancellationToken?: CancellationToken }): Promise { @@ -2840,7 +2921,7 @@ export class Repository { } async getCommit(ref: string): Promise { - const result = await this.exec(['show', '-s', '--decorate=full', '--shortstat', `--format=${COMMIT_FORMAT}`, '-z', ref]); + const result = await this.exec(['show', '-s', '--decorate=full', '--shortstat', `--format=${COMMIT_FORMAT}`, '-z', ref, '--']); const commits = parseGitCommits(result.stdout); if (commits.length === 0) { return Promise.reject('bad commit format'); @@ -2879,7 +2960,7 @@ export class Repository { async updateSubmodules(paths: string[]): Promise { const args = ['submodule', 'update']; - for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index db316c81d0b..8e5a8f9d2f1 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -163,7 +163,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // Compute base if the branch has changed const mergeBase = await this.resolveHEADMergeBase(); - this._currentHistoryItemBaseRef = mergeBase && + this._currentHistoryItemBaseRef = mergeBase && mergeBase.name && mergeBase.remote && (mergeBase.remote !== this.repository.HEAD.upstream?.remote || mergeBase.name !== this.repository.HEAD.upstream?.name) ? { id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 2f164ee5ede..f32d915d715 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -7,6 +7,7 @@ import TelemetryReporter from '@vscode/extension-telemetry'; import * as fs from 'fs'; import * as path from 'path'; import picomatch from 'picomatch'; +import * as iconv from '@vscode/iconv-lite-umd'; import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, Uri, window, workspace, WorkspaceEdit } from 'vscode'; import { ActionButton } from './actionButton'; import { ApiRepository } from './api/api1'; @@ -22,8 +23,9 @@ import { IPushErrorHandlerRegistry } from './pushError'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; -import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, isRemote, Limiter, onceEvent, pathEquals, relativePath } from './util'; +import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, isLinuxSnap, isRemote, Limiter, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; +import { detectEncoding } from './encoding'; import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -1220,10 +1222,19 @@ export class Repository implements Disposable { await this.run(Operation.Remove, () => this.repository.rm(resources.map(r => r.fsPath))); } - async stage(resource: Uri, contents: string, encoding: string): Promise { + async stage(resource: Uri, contents: string): Promise { await this.run(Operation.Stage, async () => { - const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/'); - await this.repository.stage(path, contents, encoding); + const configFiles = workspace.getConfiguration('files', Uri.file(resource.fsPath)); + let encoding = configFiles.get('encoding') ?? 'utf8'; + const autoGuessEncoding = configFiles.get('autoGuessEncoding') === true; + const candidateGuessEncodings = configFiles.get('candidateGuessEncodings') ?? []; + + if (autoGuessEncoding) { + encoding = detectEncoding(Buffer.from(contents), candidateGuessEncodings) ?? encoding; + } + + encoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; + await this.repository.stage(resource.fsPath, contents, encoding); this._onDidChangeOriginalResource.fire(resource); this.closeDiffEditors([], [...resource.fsPath]); @@ -1349,7 +1360,7 @@ export class Repository implements Disposable { async clean(resources: Uri[]): Promise { const config = workspace.getConfiguration('git'); - const untrackedChangesSoftDelete = config.get('untrackedChangesSoftDelete', true) && !isRemote; + const discardUntrackedChangesToTrash = config.get('discardUntrackedChangesToTrash', true) && !isRemote && !isLinuxSnap; await this.run( Operation.Clean(!this.optimisticUpdateEnabled()), @@ -1388,7 +1399,7 @@ export class Repository implements Disposable { } }); - if (untrackedChangesSoftDelete) { + if (discardUntrackedChangesToTrash) { const limiter = new Limiter(5); await Promise.all(toClean.map(fsPath => limiter.queue( async () => await workspace.fs.delete(Uri.file(fsPath), { useTrash: true })))); @@ -1521,7 +1532,11 @@ export class Repository implements Disposable { try { const mergeBase = await this.getConfig(mergeBaseConfigKey); const branchFromConfig = mergeBase !== '' ? await this.getBranch(mergeBase) : undefined; - if (branchFromConfig) { + + // There was a brief period of time when we would consider local branches as a valid + // merge base. Since then we have fixed the issue and only remote branches can be used + // as a merge base so we are adding an additional check. + if (branchFromConfig && branchFromConfig.remote) { return branchFromConfig; } } catch (err) { } @@ -1840,18 +1855,12 @@ export class Repository implements Disposable { await this.run(Operation.Push, () => this._push(remote, undefined, false, false, forcePushMode, true)); } - async blame(filePath: string): Promise { - return await this.run(Operation.Blame(true), () => { - const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); - return this.repository.blame(path); - }); + async blame(path: string): Promise { + return await this.run(Operation.Blame(true), () => this.repository.blame(path)); } - async blame2(filePath: string, ref?: string): Promise { - return await this.run(Operation.Blame(false), () => { - const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); - return this.repository.blame2(path, ref); - }); + async blame2(path: string, ref?: string): Promise { + return await this.run(Operation.Blame(false), () => this.repository.blame2(path, ref)); } @throttle @@ -1967,16 +1976,17 @@ export class Repository implements Disposable { async show(ref: string, filePath: string): Promise { return await this.run(Operation.Show, async () => { - const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); + const configFiles = workspace.getConfiguration('files', Uri.file(filePath)); + const defaultEncoding = configFiles.get('encoding'); + const autoGuessEncoding = configFiles.get('autoGuessEncoding'); + const candidateGuessEncodings = configFiles.get('candidateGuessEncodings'); try { - const content = await this.repository.buffer(`${ref}:${path}`); - return await workspace.decode(content, Uri.file(filePath)); + return await this.repository.bufferString(ref, filePath, defaultEncoding, autoGuessEncoding, candidateGuessEncodings); } catch (err) { if (err.gitErrorCode === GitErrorCodes.WrongCase) { - const gitRelativePath = await this.repository.getGitRelativePath(ref, path); - const content = await this.repository.buffer(`${ref}:${gitRelativePath}`); - return await workspace.decode(content, Uri.file(filePath)); + const gitFilePath = await this.repository.getGitFilePath(ref, filePath); + return await this.repository.bufferString(ref, gitFilePath, defaultEncoding, autoGuessEncoding, candidateGuessEncodings); } throw err; @@ -1985,21 +1995,15 @@ export class Repository implements Disposable { } async buffer(ref: string, filePath: string): Promise { - return this.run(Operation.Show, () => { - const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); - return this.repository.buffer(`${ref}:${path}`); - }); + return this.run(Operation.Show, () => this.repository.buffer(ref, filePath)); } getObjectFiles(ref: string): Promise { return this.run(Operation.GetObjectFiles, () => this.repository.lstree(ref)); } - getObjectDetails(ref: string, filePath: string): Promise<{ mode: string; object: string; size: number }> { - return this.run(Operation.GetObjectDetails, () => { - const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); - return this.repository.getObjectDetails(ref, path); - }); + getObjectDetails(ref: string, path: string): Promise<{ mode: string; object: string; size: number }> { + return this.run(Operation.GetObjectDetails, () => this.repository.getObjectDetails(ref, path)); } detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> { diff --git a/extensions/git/src/staging.ts b/extensions/git/src/staging.ts index 14e4e379e47..ec7232bec44 100644 --- a/extensions/git/src/staging.ts +++ b/extensions/git/src/staging.ts @@ -3,9 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, Range, LineChange, Selection, Uri, TextEditor, TextEditorDiffInformation } from 'vscode'; +import { TextDocument, Range, Selection, Uri, TextEditor, TextEditorDiffInformation } from 'vscode'; import { fromGitUri, isGitUri } from './uri'; +export interface LineChange { + readonly originalStartLineNumber: number; + readonly originalEndLineNumber: number; + readonly modifiedStartLineNumber: number; + readonly modifiedEndLineNumber: number; +} + export function applyLineChanges(original: TextDocument, modified: TextDocument, diffs: LineChange[]): string { const result: string[] = []; let currentLine = 0; diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 9565b7e5171..7dd1cbafbdf 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -12,6 +12,8 @@ import byline from 'byline'; export const isMacintosh = process.platform === 'darwin'; export const isWindows = process.platform === 'win32'; export const isRemote = env.remoteName !== undefined; +export const isLinux = process.platform === 'linux'; +export const isLinuxSnap = isLinux && !!process.env['SNAP'] && !!process.env['SNAP_REVISION']; export function log(...args: any[]): void { console.log.apply(console, ['git:', ...args]); diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 1d330ea2516..c79be520be9 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -11,7 +11,6 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts", - "../../src/vscode-dts/vscode.proposed.diffCommand.d.ts", "../../src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts", "../../src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts", "../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts", @@ -25,7 +24,6 @@ "../../src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts", "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", - "../../src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts", "../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", "../types/lib.textEncoder.d.ts" diff --git a/extensions/github-authentication/src/config.ts b/extensions/github-authentication/src/config.ts index 30b9dd66265..8cd3a9ed793 100644 --- a/extensions/github-authentication/src/config.ts +++ b/extensions/github-authentication/src/config.ts @@ -10,6 +10,10 @@ export interface IConfig { } // For easy access to mixin client ID and secret +// +// NOTE: GitHub client secrets cannot be secured when running in a native client so in other words, the client secret is +// not really a secret... so we allow the client secret in code. It is brought in before we publish VS Code. Reference: +// https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/best-practices-for-creating-an-oauth-app#client-secrets export const Config: IConfig = { gitHubClientId: '01ab8ac9400c4e429b23' }; diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index f7ee3c344e7..f43a494f66b 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -74,7 +74,8 @@ { "command": "notebook.cellOutput.addToChat", "title": "%addCellOutputToChat.title%", - "category": "Notebook" + "category": "Notebook", + "enablement": "chatIsEnabled" }, { "command": "notebook.cellOutput.openInTextEditor", diff --git a/extensions/ipynb/src/constants.ts b/extensions/ipynb/src/constants.ts index 9a82ccfae39..b72185968a8 100644 --- a/extensions/ipynb/src/constants.ts +++ b/extensions/ipynb/src/constants.ts @@ -5,7 +5,7 @@ import type { DocumentSelector } from 'vscode'; -export const defaultNotebookFormat = { major: 4, minor: 2 }; +export const defaultNotebookFormat = { major: 4, minor: 5 }; export const ATTACHMENT_CLEANUP_COMMANDID = 'ipynb.cleanInvalidImageAttachment'; export const JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR: DocumentSelector = { notebookType: 'jupyter-notebook', language: 'markdown' }; diff --git a/extensions/ipynb/src/helper.ts b/extensions/ipynb/src/helper.ts index beab091f5c6..6d67b7d529f 100644 --- a/extensions/ipynb/src/helper.ts +++ b/extensions/ipynb/src/helper.ts @@ -147,13 +147,15 @@ export interface ITask { /** * Copied from src/vs/base/common/uuid.ts */ -export function generateUuid() { - // use `randomValues` if possible - function getRandomValues(bucket: Uint8Array): Uint8Array { - for (let i = 0; i < bucket.length; i++) { - bucket[i] = Math.floor(Math.random() * 256); - } - return bucket; +export function generateUuid(): string { + // use `randomUUID` if possible + if (typeof crypto.randomUUID === 'function') { + // see https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto + // > Although crypto is available on all windows, the returned Crypto object only has one + // > usable feature in insecure contexts: the getRandomValues() method. + // > In general, you should use this API only in secure contexts. + + return crypto.randomUUID.bind(crypto)(); } // prep-work @@ -164,7 +166,7 @@ export function generateUuid() { } // get data - getRandomValues(_data); + crypto.getRandomValues(_data); // set version bits _data[6] = (_data[6] & 0x0f) | 0x40; diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts index e4fe302d188..cc55d39e112 100644 --- a/extensions/ipynb/src/ipynbMain.ts +++ b/extensions/ipynb/src/ipynbMain.ts @@ -8,6 +8,7 @@ import { activate as keepNotebookModelStoreInSync } from './notebookModelStoreSy import { notebookImagePasteSetup } from './notebookImagePaste'; import { AttachmentCleaner } from './notebookAttachmentCleaner'; import { serializeNotebookToString } from './serializers'; +import { defaultNotebookFormat } from './constants'; // From {nbformat.INotebookMetadata} in @jupyterlab/coreutils type NotebookMetadata = { @@ -86,8 +87,8 @@ export function activate(context: vscode.ExtensionContext, serializer: vscode.No data.metadata = { cells: [], metadata: {}, - nbformat: 4, - nbformat_minor: 2 + nbformat: defaultNotebookFormat.major, + nbformat_minor: defaultNotebookFormat.minor, }; const doc = await vscode.workspace.openNotebookDocument('jupyter-notebook', data); await vscode.window.showNotebookDocument(doc); diff --git a/extensions/ipynb/src/test/notebookModelStoreSync.test.ts b/extensions/ipynb/src/test/notebookModelStoreSync.test.ts index eab92167215..7174678ad61 100644 --- a/extensions/ipynb/src/test/notebookModelStoreSync.test.ts +++ b/extensions/ipynb/src/test/notebookModelStoreSync.test.ts @@ -101,7 +101,8 @@ suite(`Notebook Model Store Sync`, () => { assert.strictEqual(editsApplied.length, 0); assert.strictEqual(cellMetadataUpdates.length, 0); }); - test('Adding cell will result in an update to the metadata', async () => { + test('Adding cell to nbformat 4.2 notebook will result in adding empty metadata', async () => { + sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 2 })); const cell: NotebookCell = { document: {} as any, executionSummary: {}, @@ -131,7 +132,7 @@ suite(`Notebook Model Store Sync`, () => { const newMetadata = cellMetadataUpdates[0].newCellMetadata; assert.deepStrictEqual(newMetadata, { execution_count: null, metadata: {} }); }); - test('Add cell id if nbformat is 4.5', async () => { + test('Added cell will have a cell id if nbformat is 4.5', async () => { sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 5 })); const cell: NotebookCell = { document: {} as any, diff --git a/extensions/javascript/javascript-language-configuration.json b/extensions/javascript/javascript-language-configuration.json index c2444c453b2..20827982072 100644 --- a/extensions/javascript/javascript-language-configuration.json +++ b/extensions/javascript/javascript-language-configuration.json @@ -231,7 +231,7 @@ // Add // when pressing enter from inside line comment { "beforeText": { - "pattern": "(? { getRawData('data-initial-md-content'), 'text/html' ); - document.body.appendChild(markDownHtml.body); + document.body.append(...markDownHtml.body.children); // Restore const scrollProgress = state.scrollProgress; diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 4170a7787cf..928ffa68889 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -115,23 +115,6 @@ "tags": [ "onExP" ] - }, - "microsoft-authentication.clientIdVersion": { - "type": "string", - "default": "v1", - "enum": [ - "v2", - "v1" - ], - "enumDescriptions": [ - "%microsoft-authentication.clientIdVersion.enumDescriptions.v2%", - "%microsoft-authentication.clientIdVersion.enumDescriptions.v1%" - ], - "markdownDescription": "%microsoft-authentication.clientIdVersion.description%", - "tags": [ - "onExP", - "experimental" - ] } } } diff --git a/extensions/microsoft-authentication/package.nls.json b/extensions/microsoft-authentication/package.nls.json index ece95ac75c3..c8e0189c08f 100644 --- a/extensions/microsoft-authentication/package.nls.json +++ b/extensions/microsoft-authentication/package.nls.json @@ -12,9 +12,6 @@ }, "microsoft-authentication.implementation.enumDescriptions.msal": "Use the Microsoft Authentication Library (MSAL) to sign in with a Microsoft account.", "microsoft-authentication.implementation.enumDescriptions.classic": "(deprecated) Use the classic authentication flow to sign in with a Microsoft account.", - "microsoft-authentication.clientIdVersion.description": "The version of the Microsoft Account client ID to use for signing in with a Microsoft account. Only change this if you have been asked to. The default is `v1`.", - "microsoft-authentication.clientIdVersion.enumDescriptions.v1": "Use the v1 Microsoft Account client ID to sign in with a Microsoft account.", - "microsoft-authentication.clientIdVersion.enumDescriptions.v2": "Use the v2 Microsoft Account client ID to sign in with a Microsoft account.", "microsoft-sovereign-cloud.environment.description": { "message": "The Sovereign Cloud to use for authentication. If you select `custom`, you must also set the `#microsoft-sovereign-cloud.customEnvironment#` setting.", "comment": [ diff --git a/extensions/microsoft-authentication/src/common/accountAccess.ts b/extensions/microsoft-authentication/src/common/accountAccess.ts index a8fdeefef98..27d56b0bc1b 100644 --- a/extensions/microsoft-authentication/src/common/accountAccess.ts +++ b/extensions/microsoft-authentication/src/common/accountAccess.ts @@ -3,35 +3,52 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, SecretStorage } from 'vscode'; +import { Disposable, Event, EventEmitter, LogOutputChannel, SecretStorage } from 'vscode'; import { AccountInfo } from '@azure/msal-node'; -interface IAccountAccess { +export interface IAccountAccess { onDidAccountAccessChange: Event; isAllowedAccess(account: AccountInfo): boolean; - setAllowedAccess(account: AccountInfo, allowed: boolean): void; + setAllowedAccess(account: AccountInfo, allowed: boolean): Promise; } -export class ScopedAccountAccess implements IAccountAccess { +export class ScopedAccountAccess implements IAccountAccess, Disposable { private readonly _onDidAccountAccessChangeEmitter = new EventEmitter(); readonly onDidAccountAccessChange = this._onDidAccountAccessChangeEmitter.event; - private readonly _accountAccessSecretStorage: AccountAccessSecretStorage; - private value = new Array(); - constructor( - private readonly _secretStorage: SecretStorage, - private readonly _cloudName: string, - private readonly _clientId: string, - private readonly _authority: string + private readonly _disposable: Disposable; + + private constructor( + private readonly _accountAccessSecretStorage: IAccountAccessSecretStorage, + disposables: Disposable[] = [] ) { - this._accountAccessSecretStorage = new AccountAccessSecretStorage(this._secretStorage, this._cloudName, this._clientId, this._authority); - this._accountAccessSecretStorage.onDidChange(() => this.update()); + this._disposable = Disposable.from( + ...disposables, + this._onDidAccountAccessChangeEmitter, + this._accountAccessSecretStorage.onDidChange(() => this.update()) + ); } - initialize() { - return this.update(); + static async create( + secretStorage: SecretStorage, + cloudName: string, + logger: LogOutputChannel, + migrations: { clientId: string; authority: string }[] | undefined, + ): Promise { + const storage = await AccountAccessSecretStorage.create(secretStorage, cloudName, logger, migrations); + const access = new ScopedAccountAccess(storage, [storage]); + await access.initialize(); + return access; + } + + dispose() { + this._disposable.dispose(); + } + + private async initialize(): Promise { + await this.update(); } isAllowedAccess(account: AccountInfo): boolean { @@ -60,19 +77,26 @@ export class ScopedAccountAccess implements IAccountAccess { } } -export class AccountAccessSecretStorage { +interface IAccountAccessSecretStorage { + get(): Promise; + store(value: string[]): Thenable; + delete(): Thenable; + onDidChange: Event; +} + +class AccountAccessSecretStorage implements IAccountAccessSecretStorage, Disposable { private _disposable: Disposable; - private readonly _onDidChangeEmitter = new EventEmitter; + private readonly _onDidChangeEmitter = new EventEmitter(); readonly onDidChange: Event = this._onDidChangeEmitter.event; - private readonly _key = `accounts-${this._cloudName}-${this._clientId}-${this._authority}`; + private readonly _key = `accounts-${this._cloudName}`; - constructor( + private constructor( private readonly _secretStorage: SecretStorage, private readonly _cloudName: string, - private readonly _clientId: string, - private readonly _authority: string + private readonly _logger: LogOutputChannel, + private readonly _migrations?: { clientId: string; authority: string }[], ) { this._disposable = Disposable.from( this._onDidChangeEmitter, @@ -84,6 +108,48 @@ export class AccountAccessSecretStorage { ); } + static async create( + secretStorage: SecretStorage, + cloudName: string, + logger: LogOutputChannel, + migrations?: { clientId: string; authority: string }[], + ): Promise { + const storage = new AccountAccessSecretStorage(secretStorage, cloudName, logger, migrations); + await storage.initialize(); + return storage; + } + + /** + * TODO: Remove this method after a release with the migration + */ + private async initialize(): Promise { + if (!this._migrations) { + return; + } + const current = await this.get(); + // If the secret storage already has the new key, we have already run the migration + if (current) { + return; + } + try { + const allValues = new Set(); + for (const { clientId, authority } of this._migrations) { + const oldKey = `accounts-${this._cloudName}-${clientId}-${authority}`; + const value = await this._secretStorage.get(oldKey); + if (value) { + const parsed = JSON.parse(value) as string[]; + parsed.forEach(v => allValues.add(v)); + } + } + if (allValues.size > 0) { + await this.store(Array.from(allValues)); + } + } catch (e) { + // Migration is best effort + this._logger.error(`Failed to migrate account access secret storage: ${e}`); + } + } + async get(): Promise { const value = await this._secretStorage.get(this._key); if (!value) { diff --git a/extensions/microsoft-authentication/src/common/cachePlugin.ts b/extensions/microsoft-authentication/src/common/cachePlugin.ts index 91b4f0ee6a8..b87fdb78d9b 100644 --- a/extensions/microsoft-authentication/src/common/cachePlugin.ts +++ b/extensions/microsoft-authentication/src/common/cachePlugin.ts @@ -6,7 +6,7 @@ import { ICachePlugin, TokenCacheContext } from '@azure/msal-node'; import { Disposable, EventEmitter, SecretStorage } from 'vscode'; -export class SecretStorageCachePlugin implements ICachePlugin { +export class SecretStorageCachePlugin implements ICachePlugin, Disposable { private readonly _onDidChange: EventEmitter = new EventEmitter(); readonly onDidChange = this._onDidChange.event; diff --git a/extensions/microsoft-authentication/src/common/publicClientCache.ts b/extensions/microsoft-authentication/src/common/publicClientCache.ts index 925a4d1a88c..acc8ba0d307 100644 --- a/extensions/microsoft-authentication/src/common/publicClientCache.ts +++ b/extensions/microsoft-authentication/src/common/publicClientCache.ts @@ -2,23 +2,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { AccountInfo, AuthenticationResult, InteractiveRequest, SilentFlowRequest } from '@azure/msal-node'; +import type { AccountInfo, AuthenticationResult, InteractiveRequest, RefreshTokenRequest, SilentFlowRequest } from '@azure/msal-node'; import type { Disposable, Event } from 'vscode'; -export interface ICachedPublicClientApplication extends Disposable { - initialize(): Promise; +export interface ICachedPublicClientApplication { onDidAccountsChange: Event<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>; onDidRemoveLastAccount: Event; acquireTokenSilent(request: SilentFlowRequest): Promise; acquireTokenInteractive(request: InteractiveRequest): Promise; + acquireTokenByRefreshToken(request: RefreshTokenRequest): Promise; removeAccount(account: AccountInfo): Promise; accounts: AccountInfo[]; clientId: string; - authority: string; } export interface ICachedPublicClientApplicationManager { onDidAccountsChange: Event<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>; - getOrCreate(clientId: string, authority: string): Promise; + getOrCreate(clientId: string, refreshTokensToMigrate?: string[]): Promise; getAll(): ICachedPublicClientApplication[]; } diff --git a/extensions/microsoft-authentication/src/common/scopeData.ts b/extensions/microsoft-authentication/src/common/scopeData.ts index a43f2c431dd..88a0aad68cc 100644 --- a/extensions/microsoft-authentication/src/common/scopeData.ts +++ b/extensions/microsoft-authentication/src/common/scopeData.ts @@ -3,21 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace } from 'vscode'; - -const DEFAULT_CLIENT_ID_V1 = 'aebc6443-996d-45c2-90f0-388ff96faa56'; -const DEFAULT_TENANT_V1 = 'organizations'; -const DEFAULT_CLIENT_ID_V2 = 'c27c220f-ce2f-4904-927d-333864217eeb'; -const DEFAULT_TENANT_V2 = 'common'; +const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56'; +const DEFAULT_TENANT = 'organizations'; const OIDC_SCOPES = ['openid', 'email', 'profile', 'offline_access']; const GRAPH_TACK_ON_SCOPE = 'User.Read'; export class ScopeData { - - private readonly _defaultClientId: string; - private readonly _defaultTenant: string; - /** * The full list of scopes including: * * the original scopes passed to the constructor @@ -42,47 +34,57 @@ export class ScopeData { readonly clientId: string; /** - * The tenant ID to use for the token request. This is the value of the `VSCODE_TENANT:...` scope if present, otherwise the default tenant ID. + * The tenant ID or `organizations`, `common`, `consumers` to use for the token request. This is the value of the `VSCODE_TENANT:...` scope if present, otherwise it's the default. */ readonly tenant: string; - constructor(readonly originalScopes: readonly string[] = []) { - if (workspace.getConfiguration('microsoft-authentication').get<'v1' | 'v2'>('clientIdVersion') === 'v2') { - this._defaultClientId = DEFAULT_CLIENT_ID_V2; - this._defaultTenant = DEFAULT_TENANT_V2; - } else { - this._defaultClientId = DEFAULT_CLIENT_ID_V1; - this._defaultTenant = DEFAULT_TENANT_V1; - } + /** + * The tenant ID to use for the token request. This will only ever be a GUID if one was specified via the `VSCODE_TENANT:...` scope, otherwise undefined. + */ + readonly tenantId: string | undefined; + constructor(readonly originalScopes: readonly string[] = []) { const modifiedScopes = [...originalScopes]; modifiedScopes.sort(); this.allScopes = modifiedScopes; this.scopeStr = modifiedScopes.join(' '); this.scopesToSend = this.getScopesToSend(modifiedScopes); this.clientId = this.getClientId(this.allScopes); - this.tenant = this.getTenantId(this.allScopes); + this.tenant = this.getTenant(this.allScopes); + this.tenantId = this.getTenantId(this.tenant); } - private getClientId(scopes: string[]) { + private getClientId(scopes: string[]): string { return scopes.reduce((prev, current) => { if (current.startsWith('VSCODE_CLIENT_ID:')) { return current.split('VSCODE_CLIENT_ID:')[1]; } return prev; - }, undefined) ?? this._defaultClientId; + }, undefined) ?? DEFAULT_CLIENT_ID; } - private getTenantId(scopes: string[]) { + private getTenant(scopes: string[]): string { return scopes.reduce((prev, current) => { if (current.startsWith('VSCODE_TENANT:')) { return current.split('VSCODE_TENANT:')[1]; } return prev; - }, undefined) ?? this._defaultTenant; + }, undefined) ?? DEFAULT_TENANT; } - private getScopesToSend(scopes: string[]) { + private getTenantId(tenant: string): string | undefined { + switch (tenant) { + case 'organizations': + case 'common': + case 'consumers': + // These are not valid tenant IDs, so we return undefined + return undefined; + default: + return this.tenant; + } + } + + private getScopesToSend(scopes: string[]): string[] { const scopesToSend = scopes.filter(s => !s.startsWith('VSCODE_')); const set = new Set(scopesToSend); diff --git a/extensions/microsoft-authentication/src/common/test/scopeData.test.ts b/extensions/microsoft-authentication/src/common/test/scopeData.test.ts index 9250d7cecbd..4c70e4fd07c 100644 --- a/extensions/microsoft-authentication/src/common/test/scopeData.test.ts +++ b/extensions/microsoft-authentication/src/common/test/scopeData.test.ts @@ -56,4 +56,21 @@ suite('ScopeData', () => { const scopeData = new ScopeData(['custom_scope', 'VSCODE_TENANT:some_tenant']); assert.strictEqual(scopeData.tenant, 'some_tenant'); }); + + test('should have tenantId be undefined if no VSCODE_TENANT scope is present', () => { + const scopeData = new ScopeData(['custom_scope']); + assert.strictEqual(scopeData.tenantId, undefined); + }); + + test('should have tenantId be undefined if typical tenant values are present', () => { + for (const element of ['common', 'organizations', 'consumers']) { + const scopeData = new ScopeData(['custom_scope', `VSCODE_TENANT:${element}`]); + assert.strictEqual(scopeData.tenantId, undefined); + } + }); + + test('should have tenantId be the value of VSCODE_TENANT scope if set to a specific value', () => { + const scopeData = new ScopeData(['custom_scope', 'VSCODE_TENANT:some_guid']); + assert.strictEqual(scopeData.tenantId, 'some_guid'); + }); }); diff --git a/extensions/microsoft-authentication/src/extensionV2.ts b/extensions/microsoft-authentication/src/extensionV2.ts index 9610af37977..978603ad132 100644 --- a/extensions/microsoft-authentication/src/extensionV2.ts +++ b/extensions/microsoft-authentication/src/extensionV2.ts @@ -49,14 +49,13 @@ async function initMicrosoftSovereignCloudAuthProvider( return undefined; } - const authProvider = new MsalAuthProvider( + const authProvider = await MsalAuthProvider.create( context, new MicrosoftSovereignCloudAuthenticationTelemetryReporter(context.extension.packageJSON.aiKey), window.createOutputChannel(l10n.t('Microsoft Sovereign Cloud Authentication'), { log: true }), uriHandler, env ); - await authProvider.initialize(); const disposable = authentication.registerAuthenticationProvider( 'microsoft-sovereign-cloud', authProviderName, @@ -70,13 +69,12 @@ async function initMicrosoftSovereignCloudAuthProvider( export async function activate(context: ExtensionContext, mainTelemetryReporter: MicrosoftAuthenticationTelemetryReporter) { const uriHandler = new UriEventHandler(); context.subscriptions.push(uriHandler); - const authProvider = new MsalAuthProvider( + const authProvider = await MsalAuthProvider.create( context, mainTelemetryReporter, Logger, uriHandler ); - await authProvider.initialize(); context.subscriptions.push(authentication.registerAuthenticationProvider( 'microsoft', 'Microsoft', diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index 40000e08620..a26008fb780 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -7,7 +7,7 @@ import { AuthenticationGetSessionOptions, AuthenticationProvider, Authentication import { Environment } from '@azure/ms-rest-azure-env'; import { CachedPublicClientApplicationManager } from './publicClientCache'; import { UriEventHandler } from '../UriEventHandler'; -import { ICachedPublicClientApplication } from '../common/publicClientCache'; +import { ICachedPublicClientApplication, ICachedPublicClientApplicationManager } from '../common/publicClientCache'; import { MicrosoftAccountType, MicrosoftAuthenticationTelemetryReporter } from '../common/telemetryReporter'; import { ScopeData } from '../common/scopeData'; import { EventBufferer } from '../common/event'; @@ -22,7 +22,6 @@ const MSA_PASSTHRU_TID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a'; export class MsalAuthProvider implements AuthenticationProvider { private readonly _disposables: { dispose(): void }[]; - private readonly _publicClientManager: CachedPublicClientApplicationManager; private readonly _eventBufferer = new EventBufferer(); /** @@ -43,20 +42,15 @@ export class MsalAuthProvider implements AuthenticationProvider { */ onDidChangeSessions = this._onDidChangeSessionsEmitter.event; - constructor( + private constructor( private readonly _context: ExtensionContext, private readonly _telemetryReporter: MicrosoftAuthenticationTelemetryReporter, private readonly _logger: LogOutputChannel, private readonly _uriHandler: UriEventHandler, + private readonly _publicClientManager: ICachedPublicClientApplicationManager, private readonly _env: Environment = Environment.AzureCloud ) { this._disposables = _context.subscriptions; - this._publicClientManager = new CachedPublicClientApplicationManager( - _context.globalState, - _context.secrets, - this._logger, - this._env.name - ); const accountChangeEvent = this._eventBufferer.wrapEvent( this._publicClientManager.onDidAccountsChange, (last, newEvent) => { @@ -81,11 +75,24 @@ export class MsalAuthProvider implements AuthenticationProvider { )(e => this._handleAccountChange(e)); this._disposables.push( this._onDidChangeSessionsEmitter, - this._publicClientManager, accountChangeEvent ); } + static async create( + context: ExtensionContext, + telemetryReporter: MicrosoftAuthenticationTelemetryReporter, + logger: LogOutputChannel, + uriHandler: UriEventHandler, + env: Environment = Environment.AzureCloud + ): Promise { + const publicClientManager = await CachedPublicClientApplicationManager.create(context.secrets, logger, env.name); + context.subscriptions.push(publicClientManager); + const authProvider = new MsalAuthProvider(context, telemetryReporter, logger, uriHandler, publicClientManager, env); + await authProvider.initialize(); + return authProvider; + } + /** * Migrate sessions from the old secret storage to MSAL. * TODO: MSAL Migration. Remove this when we remove the old flow. @@ -109,14 +116,12 @@ export class MsalAuthProvider implements AuthenticationProvider { clientTenantMap.get(key)!.refreshTokens.push(session.refreshToken); } - for (const { clientId, tenant, refreshTokens } of clientTenantMap.values()) { - await this.getOrCreatePublicClientApplication(clientId, tenant, refreshTokens); + for (const { clientId, refreshTokens } of clientTenantMap.values()) { + await this._publicClientManager.getOrCreate(clientId, refreshTokens); } } - async initialize(): Promise { - await this._eventBufferer.bufferEventsAsync(() => this._publicClientManager.initialize()); - + private async initialize(): Promise { if (!this._context.globalState.get('msalMigration', false)) { await this._migrateSessions(); } @@ -173,8 +178,8 @@ export class MsalAuthProvider implements AuthenticationProvider { return allSessions; } - const cachedPca = await this.getOrCreatePublicClientApplication(scopeData.clientId, scopeData.tenant); - const sessions = await this.getAllSessionsForPca(cachedPca, scopeData.originalScopes, scopeData.scopesToSend, options?.account); + const cachedPca = await this._publicClientManager.getOrCreate(scopeData.clientId); + const sessions = await this.getAllSessionsForPca(cachedPca, scopeData, options?.account); this._logger.info(`[getSessions] [${scopeData.scopeStr}] returned ${sessions.length} session(s)`); return sessions; @@ -185,7 +190,7 @@ export class MsalAuthProvider implements AuthenticationProvider { // Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead. this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'starting'); - const cachedPca = await this.getOrCreatePublicClientApplication(scopeData.clientId, scopeData.tenant); + const cachedPca = await this._publicClientManager.getOrCreate(scopeData.clientId); // Used for showing a friendlier message to the user when the explicitly cancel a flow. let userCancelled: boolean | undefined; @@ -211,6 +216,7 @@ export class MsalAuthProvider implements AuthenticationProvider { : ExtensionHost.WebWorker, }); + const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString(); let lastError: Error | undefined; for (const flow of flows) { if (flow !== flows[0]) { @@ -223,6 +229,7 @@ export class MsalAuthProvider implements AuthenticationProvider { try { const result = await flow.trigger({ cachedPca, + authority, scopes: scopeData.scopesToSend, loginHint: options.account?.label, windowHandle: window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined, @@ -260,7 +267,7 @@ export class MsalAuthProvider implements AuthenticationProvider { if (account.homeAccountId === sessionId) { this._telemetryReporter.sendLogoutEvent(); promises.push(cachedPca.removeAccount(account)); - this._logger.info(`[removeSession] [${sessionId}] [${cachedPca.clientId}] [${cachedPca.authority}] removing session...`); + this._logger.info(`[removeSession] [${sessionId}] [${cachedPca.clientId}] removing session...`); } } } @@ -281,26 +288,69 @@ export class MsalAuthProvider implements AuthenticationProvider { //#endregion - private async getOrCreatePublicClientApplication(clientId: string, tenant: string, refreshTokensToMigrate?: string[]): Promise { - const authority = new URL(tenant, this._env.activeDirectoryEndpointUrl).toString(); - return await this._publicClientManager.getOrCreate(clientId, authority, refreshTokensToMigrate); - } - private async getAllSessionsForPca( cachedPca: ICachedPublicClientApplication, - originalScopes: readonly string[], - scopesToSend: string[], + scopeData: ScopeData, accountFilter?: AuthenticationSessionAccountInformation ): Promise { - const accounts = accountFilter + let filteredAccounts = accountFilter ? cachedPca.accounts.filter(a => a.homeAccountId === accountFilter.id) : cachedPca.accounts; + + // Group accounts by homeAccountId + const accountGroups = new Map(); + for (const account of filteredAccounts) { + const existing = accountGroups.get(account.homeAccountId) || []; + existing.push(account); + accountGroups.set(account.homeAccountId, existing); + } + + // Filter to one account per homeAccountId + filteredAccounts = Array.from(accountGroups.values()).map(accounts => { + if (accounts.length === 1) { + return accounts[0]; + } + + // If we have a specific tenant to target, prefer that one + if (scopeData.tenantId) { + const matchingTenant = accounts.find(a => a.tenantId === scopeData.tenantId); + if (matchingTenant) { + return matchingTenant; + } + } + + // Otherwise prefer the home tenant + return accounts.find(a => a.tenantId === a.idTokenClaims?.tid) || accounts[0]; + }); + + const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString(); const sessions: AuthenticationSession[] = []; return this._eventBufferer.bufferEventsAsync(async () => { - for (const account of accounts) { + for (const account of filteredAccounts) { try { - const result = await cachedPca.acquireTokenSilent({ account, scopes: scopesToSend, redirectUri }); - sessions.push(this.sessionFromAuthenticationResult(result, originalScopes)); + let forceRefresh: true | undefined; + if (scopeData.tenantId) { + // If the tenants do not match, then we need to skip the cache + // to get a new token for the new tenant + if (account.tenantId !== scopeData.tenantId) { + forceRefresh = true; + } + } else { + // If we are requesting the home tenant and we don't yet have + // a token for the home tenant, we need to skip the cache + // to get a new token for the home tenant + if (account.tenantId !== account.idTokenClaims?.tid) { + forceRefresh = true; + } + } + const result = await cachedPca.acquireTokenSilent({ + account, + authority, + scopes: scopeData.scopesToSend, + redirectUri, + forceRefresh + }); + sessions.push(this.sessionFromAuthenticationResult(result, scopeData.originalScopes)); } catch (e) { // If we can't get a token silently, the account is probably in a bad state so we should skip it // MSAL will log this already, so we don't need to log it again diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index 8d081f1a825..f0f4eb7b9bc 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PublicClientApplication, AccountInfo, Configuration, SilentFlowRequest, AuthenticationResult, InteractiveRequest, LogLevel, RefreshTokenRequest } from '@azure/msal-node'; +import { PublicClientApplication, AccountInfo, SilentFlowRequest, AuthenticationResult, InteractiveRequest, LogLevel, RefreshTokenRequest } from '@azure/msal-node'; import { NativeBrokerPlugin } from '@azure/msal-node-extensions'; -import { Disposable, Memento, SecretStorage, LogOutputChannel, window, ProgressLocation, l10n, EventEmitter } from 'vscode'; +import { Disposable, SecretStorage, LogOutputChannel, window, ProgressLocation, l10n, EventEmitter } from 'vscode'; import { raceCancellationAndTimeoutError } from '../common/async'; import { SecretStorageCachePlugin } from '../common/cachePlugin'; import { MsalLoggerOptions } from '../common/loggerOptions'; import { ICachedPublicClientApplication } from '../common/publicClientCache'; -import { ScopedAccountAccess } from '../common/accountAccess'; +import { IAccountAccess } from '../common/accountAccess'; export class CachedPublicClientApplication implements ICachedPublicClientApplication { // Core properties @@ -23,11 +23,10 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica private readonly _secretStorageCachePlugin = new SecretStorageCachePlugin( this._secretStorage, // Include the prefix as a differentiator to other secrets - `pca:${JSON.stringify({ clientId: this._clientId, authority: this._authority })}` + `pca:${this._clientId}` ); // Broker properties - private readonly _accountAccess = new ScopedAccountAccess(this._secretStorage, this._cloudName, this._clientId, this._authority); private readonly _isBrokerAvailable: boolean; //#region Events @@ -40,24 +39,20 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica //#endregion - constructor( + private constructor( private readonly _clientId: string, - private readonly _authority: string, - private readonly _cloudName: string, - private readonly _globalMemento: Memento, private readonly _secretStorage: SecretStorage, - private readonly _logger: LogOutputChannel + private readonly _accountAccess: IAccountAccess, + private readonly _logger: LogOutputChannel, ) { - // TODO:@TylerLeonhardt clean up old use of memento. Remove this in an iteration - this._globalMemento.update(`lastRemoval:${this._clientId}:${this._authority}`, undefined); const loggerOptions = new MsalLoggerOptions(_logger); const nativeBrokerPlugin = new NativeBrokerPlugin(); - this._isBrokerAvailable = nativeBrokerPlugin.isBrokerAvailable ?? false; + this._isBrokerAvailable = nativeBrokerPlugin.isBrokerAvailable; this._pca = new PublicClientApplication({ - auth: { clientId: _clientId, authority: _authority }, + auth: { clientId: _clientId }, system: { loggerOptions: { - correlationId: `${_clientId}] [${_authority}`, + correlationId: _clientId, loggerCallback: (level, message, containsPii) => loggerOptions.loggerCallback(level, message, containsPii), logLevel: LogLevel.Trace } @@ -68,18 +63,26 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica this._disposable = Disposable.from( this._registerOnSecretStorageChanged(), this._onDidAccountsChangeEmitter, - this._onDidRemoveLastAccountEmitter + this._onDidRemoveLastAccountEmitter, + this._secretStorageCachePlugin ); } get accounts(): AccountInfo[] { return this._accounts; } get clientId(): string { return this._clientId; } - get authority(): string { return this._authority; } - async initialize(): Promise { - if (this._isBrokerAvailable) { - await this._accountAccess.initialize(); - } + static async create( + clientId: string, + secretStorage: SecretStorage, + accountAccess: IAccountAccess, + logger: LogOutputChannel + ): Promise { + const app = new CachedPublicClientApplication(clientId, secretStorage, accountAccess, logger); + await app.initialize(); + return app; + } + + private async initialize(): Promise { await this._sequencer.queue(() => this._update()); } @@ -88,9 +91,9 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica } async acquireTokenSilent(request: SilentFlowRequest): Promise { - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] starting...`); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] starting...`); let result = await this._sequencer.queue(() => this._pca.acquireTokenSilent(request)); - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got result`); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] got result`); // Check expiration of id token and if it's 5min before expiration, force a refresh. // this is what MSAL does for access tokens already so we're just adding it for id tokens since we care about those. // NOTE: Once we stop depending on id tokens for some things we can remove all of this. @@ -101,13 +104,13 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica * 1000 // convert to milliseconds ); if (fiveMinutesBefore < new Date()) { - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is expired or about to expire. Forcing refresh...`); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is expired or about to expire. Forcing refresh...`); const newRequest = this._isBrokerAvailable // HACK: Broker doesn't support forceRefresh so we need to pass in claims which will force a refresh ? { ...request, claims: '{ "id_token": {}}' } : { ...request, forceRefresh: true }; result = await this._sequencer.queue(() => this._pca.acquireTokenSilent(newRequest)); - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got forced result`); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] got forced result`); } const newIdTokenExpirationInSecs = (result.idTokenClaims as { exp?: number }).exp; if (newIdTokenExpirationInSecs) { @@ -116,15 +119,15 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica * 1000 // convert to milliseconds ); if (fiveMinutesBefore < new Date()) { - this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is still expired.`); + this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is still expired.`); // HACK: Only for the Broker we try one more time with different claims to force a refresh. Why? We've seen the Broker caching tokens by the claims requested, thus // there has been a situation where both tokens are expired. if (this._isBrokerAvailable) { - this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] forcing refresh with different claims...`); + this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] forcing refresh with different claims...`); const newRequest = { ...request, claims: '{ "access_token": {}}' }; result = await this._sequencer.queue(() => this._pca.acquireTokenSilent(newRequest)); - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got forced result with different claims`); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] got forced result with different claims`); const newIdTokenExpirationInSecs = (result.idTokenClaims as { exp?: number }).exp; if (newIdTokenExpirationInSecs) { const fiveMinutesBefore = new Date( @@ -132,7 +135,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica * 1000 // convert to milliseconds ); if (fiveMinutesBefore < new Date()) { - this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is still expired.`); + this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is still expired.`); } } } @@ -140,15 +143,17 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica } } - if (result.account && !result.fromCache && this._verifyIfUsingBroker(result)) { - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] firing event due to change`); + if (!result.account) { + this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] no account found in result`); + } else if (!result.fromCache && this._verifyIfUsingBroker(result)) { + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] firing event due to change`); this._onDidAccountsChangeEmitter.fire({ added: [], changed: [result.account], deleted: [] }); } return result; } async acquireTokenInteractive(request: InteractiveRequest): Promise { - this._logger.debug(`[acquireTokenInteractive] [${this._clientId}] [${this._authority}] [${request.scopes?.join(' ')}] loopbackClientOverride: ${request.loopbackClient ? 'true' : 'false'}`); + this._logger.debug(`[acquireTokenInteractive] [${this._clientId}] [${request.authority}] [${request.scopes?.join(' ')}] loopbackClientOverride: ${request.loopbackClient ? 'true' : 'false'}`); return await window.withProgress( { location: ProgressLocation.Notification, @@ -180,9 +185,17 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica * @param request a {@link RefreshTokenRequest} object that contains the refresh token and other parameters. * @returns an {@link AuthenticationResult} object that contains the result of the token acquisition operation. */ - async acquireTokenByRefreshToken(request: RefreshTokenRequest) { - this._logger.debug(`[acquireTokenByRefreshToken] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}]`); - const result = await this._sequencer.queue(() => this._pca.acquireTokenByRefreshToken(request)); + async acquireTokenByRefreshToken(request: RefreshTokenRequest): Promise { + this._logger.debug(`[acquireTokenByRefreshToken] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}]`); + const result = await this._sequencer.queue(async () => { + const result = await this._pca.acquireTokenByRefreshToken(request); + // Force an update so that the account cache is updated. + // TODO:@TylerLeonhardt The problem is, we use the sequencer for + // change events but we _don't_ use it for the accounts cache. + // We should probably use it for the accounts cache as well. + await this._update(); + return result; + }); if (result) { // this._setupRefresh(result); if (this._isBrokerAvailable && result.account) { @@ -213,7 +226,14 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica if (!result.fromNativeBroker) { return true; } - const key = result.account!.homeAccountId; + // The nativeAccountId is what the broker uses to differenciate all + // types of accounts. Even if the "account" is a duplicate of another because + // it's actaully a guest account in another tenant. + let key = result.account!.nativeAccountId; + if (!key) { + this._logger.error(`[verifyIfUsingBroker] [${this._clientId}] [${result.account!.username}] no nativeAccountId found. Using homeAccountId instead.`); + key = result.account!.homeAccountId; + } const lastSeen = this._lastSeen.get(key); const lastTimeAuthed = result.account!.idTokenClaims!.iat!; if (!lastSeen) { @@ -229,7 +249,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica private async _update() { const before = this._accounts; - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update before: ${before.length}`); + this._logger.debug(`[update] [${this._clientId}] CachedPublicClientApplication update before: ${before.length}`); // Clear in-memory cache so we know we're getting account data from the SecretStorage this._pca.clearCache(); let after = await this._pca.getAllAccounts(); @@ -237,7 +257,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica after = after.filter(a => this._accountAccess.isAllowedAccess(a)); } this._accounts = after; - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update after: ${after.length}`); + this._logger.debug(`[update] [${this._clientId}] CachedPublicClientApplication update after: ${after.length}`); const beforeSet = new Set(before.map(b => b.homeAccountId)); const afterSet = new Set(after.map(a => a.homeAccountId)); @@ -246,13 +266,13 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica const deleted = before.filter(b => !afterSet.has(b.homeAccountId)); if (added.length > 0 || deleted.length > 0) { this._onDidAccountsChangeEmitter.fire({ added, changed: [], deleted }); - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication accounts changed. added: ${added.length}, deleted: ${deleted.length}`); + this._logger.debug(`[update] [${this._clientId}] CachedPublicClientApplication accounts changed. added: ${added.length}, deleted: ${deleted.length}`); if (!after.length) { - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication final account deleted. Firing event.`); + this._logger.debug(`[update] [${this._clientId}] CachedPublicClientApplication final account deleted. Firing event.`); this._onDidRemoveLastAccountEmitter.fire(); } } - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update complete`); + this._logger.debug(`[update] [${this._clientId}] CachedPublicClientApplication update complete`); } } diff --git a/extensions/microsoft-authentication/src/node/flows.ts b/extensions/microsoft-authentication/src/node/flows.ts index c6f40a943f6..ac678d8313d 100644 --- a/extensions/microsoft-authentication/src/node/flows.ts +++ b/extensions/microsoft-authentication/src/node/flows.ts @@ -25,6 +25,7 @@ interface IMsalFlowOptions { interface IMsalFlowTriggerOptions { cachedPca: ICachedPublicClientApplication; + authority: string; scopes: string[]; loginHint?: string; windowHandle?: Buffer; @@ -45,11 +46,12 @@ class DefaultLoopbackFlow implements IMsalFlow { supportsWebWorkerExtensionHost: false }; - async trigger({ cachedPca, scopes, loginHint, windowHandle, logger }: IMsalFlowTriggerOptions): Promise { + async trigger({ cachedPca, authority, scopes, loginHint, windowHandle, logger }: IMsalFlowTriggerOptions): Promise { logger.info('Trying default msal flow...'); return await cachedPca.acquireTokenInteractive({ openBrowser: async (url: string) => { await env.openExternal(Uri.parse(url)); }, scopes, + authority, successTemplate: loopbackTemplate, errorTemplate: loopbackTemplate, loginHint, @@ -66,12 +68,13 @@ class UrlHandlerFlow implements IMsalFlow { supportsWebWorkerExtensionHost: false }; - async trigger({ cachedPca, scopes, loginHint, windowHandle, logger, uriHandler }: IMsalFlowTriggerOptions): Promise { + async trigger({ cachedPca, authority, scopes, loginHint, windowHandle, logger, uriHandler }: IMsalFlowTriggerOptions): Promise { logger.info('Trying protocol handler flow...'); const loopbackClient = new UriHandlerLoopbackClient(uriHandler, redirectUri, logger); return await cachedPca.acquireTokenInteractive({ openBrowser: (url: string) => loopbackClient.openBrowser(url), scopes, + authority, loopbackClient, loginHint, prompt: loginHint ? undefined : 'select_account', diff --git a/extensions/microsoft-authentication/src/node/publicClientCache.ts b/extensions/microsoft-authentication/src/node/publicClientCache.ts index f4f19ff8df5..16ccb80321f 100644 --- a/extensions/microsoft-authentication/src/node/publicClientCache.ts +++ b/extensions/microsoft-authentication/src/node/publicClientCache.ts @@ -7,6 +7,7 @@ import { AccountInfo } from '@azure/msal-node'; import { SecretStorage, LogOutputChannel, Disposable, EventEmitter, Memento, Event } from 'vscode'; import { ICachedPublicClientApplication, ICachedPublicClientApplicationManager } from '../common/publicClientCache'; import { CachedPublicClientApplication } from './cachedPublicClientApplication'; +import { IAccountAccess, ScopedAccountAccess } from '../common/accountAccess'; export interface IPublicClientApplicationInfo { clientId: string; @@ -14,56 +15,68 @@ export interface IPublicClientApplicationInfo { } export class CachedPublicClientApplicationManager implements ICachedPublicClientApplicationManager { - // The key is the clientId and authority JSON stringified - private readonly _pcas = new Map(); + // The key is the clientId + private readonly _pcas = new Map(); private readonly _pcaDisposables = new Map(); private _disposable: Disposable; - private _pcasSecretStorage: PublicClientApplicationsSecretStorage; private readonly _onDidAccountsChangeEmitter = new EventEmitter<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>(); readonly onDidAccountsChange = this._onDidAccountsChangeEmitter.event; - constructor( - private readonly _globalMemento: Memento, + private constructor( + private readonly _pcasSecretStorage: IPublicClientApplicationSecretStorage, + private readonly _accountAccess: IAccountAccess, private readonly _secretStorage: SecretStorage, private readonly _logger: LogOutputChannel, - private readonly _cloudName: string + disposables: Disposable[] ) { - this._pcasSecretStorage = new PublicClientApplicationsSecretStorage(_secretStorage, _cloudName); this._disposable = Disposable.from( - this._pcasSecretStorage, + ...disposables, this._registerSecretStorageHandler(), this._onDidAccountsChangeEmitter ); } + static async create( + secretStorage: SecretStorage, + logger: LogOutputChannel, + cloudName: string + ): Promise { + const pcasSecretStorage = await PublicClientApplicationsSecretStorage.create(secretStorage, cloudName); + // TODO: Remove the migrations in a version + const migrations = await pcasSecretStorage.getOldValue(); + const accountAccess = await ScopedAccountAccess.create(secretStorage, cloudName, logger, migrations); + const manager = new CachedPublicClientApplicationManager(pcasSecretStorage, accountAccess, secretStorage, logger, [pcasSecretStorage, accountAccess]); + await manager.initialize(); + return manager; + } + private _registerSecretStorageHandler() { return this._pcasSecretStorage.onDidChange(() => this._handleSecretStorageChange()); } - async initialize() { + private async initialize() { this._logger.debug('[initialize] Initializing PublicClientApplicationManager'); - let keys: string[] | undefined; + let clientIds: string[] | undefined; try { - keys = await this._pcasSecretStorage.get(); + clientIds = await this._pcasSecretStorage.get(); } catch (e) { // data is corrupted this._logger.error('[initialize] Error initializing PublicClientApplicationManager:', e); await this._pcasSecretStorage.delete(); } - if (!keys) { + if (!clientIds) { return; } const promises = new Array>(); - for (const key of keys) { + for (const clientId of clientIds) { try { - const { clientId, authority } = JSON.parse(key) as IPublicClientApplicationInfo; // Load the PCA in memory - promises.push(this._doCreatePublicClientApplication(clientId, authority, key)); + promises.push(this._doCreatePublicClientApplication(clientId)); } catch (e) { - this._logger.error('[initialize] Error intitializing PCA:', key); + this._logger.error('[initialize] Error intitializing PCA:', clientId); } } @@ -75,11 +88,11 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient } else { if (!result.value.accounts.length) { pcasChanged = true; - const pcaKey = JSON.stringify({ clientId: result.value.clientId, authority: result.value.authority }); - this._pcaDisposables.get(pcaKey)?.dispose(); - this._pcaDisposables.delete(pcaKey); - this._pcas.delete(pcaKey); - this._logger.debug(`[initialize] [${result.value.clientId}] [${result.value.authority}] PCA disposed because it's empty.`); + const clientId = result.value.clientId; + this._pcaDisposables.get(clientId)?.dispose(); + this._pcaDisposables.delete(clientId); + this._pcas.delete(clientId); + this._logger.debug(`[initialize] [${clientId}] PCA disposed because it's empty.`); } } } @@ -94,43 +107,39 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient Disposable.from(...this._pcaDisposables.values()).dispose(); } - async getOrCreate(clientId: string, authority: string, refreshTokensToMigrate?: string[]): Promise { - // Use the clientId and authority as the key - const pcasKey = JSON.stringify({ clientId, authority }); - let pca = this._pcas.get(pcasKey); + async getOrCreate(clientId: string, refreshTokensToMigrate?: string[]): Promise { + let pca = this._pcas.get(clientId); if (pca) { - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PublicClientApplicationManager cache hit`); + this._logger.debug(`[getOrCreate] [${clientId}] PublicClientApplicationManager cache hit`); } else { - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PublicClientApplicationManager cache miss, creating new PCA...`); - pca = await this._doCreatePublicClientApplication(clientId, authority, pcasKey); + this._logger.debug(`[getOrCreate] [${clientId}] PublicClientApplicationManager cache miss, creating new PCA...`); + pca = await this._doCreatePublicClientApplication(clientId); await this._storePublicClientApplications(); - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PCA created.`); + this._logger.debug(`[getOrCreate] [${clientId}] PCA created.`); } // TODO: MSAL Migration. Remove this when we remove the old flow. if (refreshTokensToMigrate?.length) { - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] Migrating refresh tokens to PCA...`); + this._logger.debug(`[getOrCreate] [${clientId}] Migrating refresh tokens to PCA...`); for (const refreshToken of refreshTokensToMigrate) { try { // Use the refresh token to acquire a result. This will cache the refresh token for future operations. // The scopes don't matter here since we can create any token from the refresh token. const result = await pca.acquireTokenByRefreshToken({ refreshToken, forceCache: true, scopes: [] }); if (result?.account) { - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] Refresh token migrated to PCA.`); + this._logger.debug(`[getOrCreate] [${clientId}] Refresh token migrated to PCA.`); } } catch (e) { - this._logger.error(`[getOrCreate] [${clientId}] [${authority}] Error migrating refresh token:`, e); + this._logger.error(`[getOrCreate] [${clientId}] Error migrating refresh token:`, e); } } - // reinitialize the PCA so the account is properly cached - await pca.initialize(); } return pca; } - private async _doCreatePublicClientApplication(clientId: string, authority: string, pcasKey: string) { - const pca = new CachedPublicClientApplication(clientId, authority, this._cloudName, this._globalMemento, this._secretStorage, this._logger); - this._pcas.set(pcasKey, pca); + private async _doCreatePublicClientApplication(clientId: string): Promise { + const pca = await CachedPublicClientApplication.create(clientId, this._secretStorage, this._accountAccess, this._logger); + this._pcas.set(clientId, pca); const disposable = Disposable.from( pca, pca.onDidAccountsChange(e => this._onDidAccountsChangeEmitter.fire(e)), @@ -138,15 +147,17 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient // The PCA has no more accounts, so we can dispose it so we're not keeping it // around forever. disposable.dispose(); - this._pcaDisposables.delete(pcasKey); - this._pcas.delete(pcasKey); - this._logger.debug(`[_doCreatePublicClientApplication] [${clientId}] [${authority}] PCA disposed. Firing off storing of PCAs...`); + this._pcaDisposables.delete(clientId); + this._pcas.delete(clientId); + this._logger.debug(`[_doCreatePublicClientApplication] [${clientId}] PCA disposed. Firing off storing of PCAs...`); void this._storePublicClientApplications(); }) ); - this._pcaDisposables.set(pcasKey, disposable); - // Intialize the PCA after the `onDidAccountsChange` is set so we get initial state. - await pca.initialize(); + this._pcaDisposables.set(clientId, disposable); + // Fire for the initial state and only if accounts exist + if (pca.accounts.length > 0) { + this._onDidAccountsChangeEmitter.fire({ added: pca.accounts, changed: [], deleted: [] }); + } return pca; } @@ -183,15 +194,14 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient } // Handle the new ones - for (const newPca of pcaKeysFromStorage) { + for (const clientId of pcaKeysFromStorage) { try { - const { clientId, authority } = JSON.parse(newPca); - this._logger.debug(`[_handleSecretStorageChange] [${clientId}] [${authority}] Creating new PCA that was created in another window...`); - await this._doCreatePublicClientApplication(clientId, authority, newPca); - this._logger.debug(`[_handleSecretStorageChange] [${clientId}] [${authority}] PCA created.`); + this._logger.debug(`[_handleSecretStorageChange] [${clientId}] Creating new PCA that was created in another window...`); + await this._doCreatePublicClientApplication(clientId); + this._logger.debug(`[_handleSecretStorageChange] [${clientId}] PCA created.`); } catch (_e) { // This really shouldn't happen, but should we do something about this? - this._logger.error(`Failed to parse new PublicClientApplication: ${newPca}`); + this._logger.error(`Failed to create new PublicClientApplication: ${clientId}`); continue; } } @@ -204,15 +214,24 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient } } -class PublicClientApplicationsSecretStorage { +interface IPublicClientApplicationSecretStorage { + get(): Promise; + getOldValue(): Promise<{ clientId: string; authority: string }[] | undefined>; + store(value: string[]): Thenable; + delete(): Thenable; + onDidChange: Event; +} + +class PublicClientApplicationsSecretStorage implements IPublicClientApplicationSecretStorage, Disposable { private _disposable: Disposable; private readonly _onDidChangeEmitter = new EventEmitter; readonly onDidChange: Event = this._onDidChangeEmitter.event; - private readonly _key = `publicClientApplications-${this._cloudName}`; + private readonly _oldKey = `publicClientApplications-${this._cloudName}`; + private readonly _key = `publicClients-${this._cloudName}`; - constructor(private readonly _secretStorage: SecretStorage, private readonly _cloudName: string) { + private constructor(private readonly _secretStorage: SecretStorage, private readonly _cloudName: string) { this._disposable = Disposable.from( this._onDidChangeEmitter, this._secretStorage.onDidChange(e => { @@ -223,6 +242,30 @@ class PublicClientApplicationsSecretStorage { ); } + static async create(secretStorage: SecretStorage, cloudName: string): Promise { + const storage = new PublicClientApplicationsSecretStorage(secretStorage, cloudName); + await storage.initialize(); + return storage; + } + + /** + * Runs the migration. + * TODO: Remove this after a version. + */ + private async initialize() { + const oldValue = await this.getOldValue(); + if (!oldValue) { + return; + } + const newValue = await this.get() ?? []; + for (const { clientId } of oldValue) { + if (!newValue.includes(clientId)) { + newValue.push(clientId); + } + } + await this.store(newValue); + } + async get(): Promise { const value = await this._secretStorage.get(this._key); if (!value) { @@ -231,6 +274,25 @@ class PublicClientApplicationsSecretStorage { return JSON.parse(value); } + /** + * Old representation of data that included the authority. This should be removed in a version or 2. + * @returns An array of objects with clientId and authority + */ + async getOldValue(): Promise<{ clientId: string; authority: string }[] | undefined> { + const value = await this._secretStorage.get(this._oldKey); + if (!value) { + return undefined; + } + const result: { clientId: string; authority: string }[] = []; + for (const stringifiedObj of JSON.parse(value)) { + const obj = JSON.parse(stringifiedObj); + if (obj.clientId && obj.authority) { + result.push(obj); + } + } + return result; + } + store(value: string[]): Thenable { return this._secretStorage.store(this._key, JSON.stringify(value)); } diff --git a/extensions/npm/.vscode/launch.json b/extensions/npm/.vscode/launch.json index 017c8762415..b5cc96144bc 100644 --- a/extensions/npm/.vscode/launch.json +++ b/extensions/npm/.vscode/launch.json @@ -9,10 +9,7 @@ "args": [ "--extensionDevelopmentPath=${workspaceFolder}" ], - "stopOnEntry": false, "sourceMaps": true, - "outFiles": ["${workspaceFolder}/client/out/**/*.js"], - "preLaunchTask": "npm" } ] -} \ No newline at end of file +} diff --git a/extensions/npm/src/npmMain.ts b/extensions/npm/src/npmMain.ts index dc1c2c24126..c37056fc4a6 100644 --- a/extensions/npm/src/npmMain.ts +++ b/extensions/npm/src/npmMain.ts @@ -38,7 +38,7 @@ export async function activate(context: vscode.ExtensionContext): Promise treeDataProvider = registerExplorer(context); context.subscriptions.push(vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration('npm.exclude') || e.affectsConfiguration('npm.autoDetect') || e.affectsConfiguration('npm.scriptExplorerExclude')) { + if (e.affectsConfiguration('npm.exclude') || e.affectsConfiguration('npm.autoDetect') || e.affectsConfiguration('npm.scriptExplorerExclude') || e.affectsConfiguration('npm.runSilent') || e.affectsConfiguration('npm.packageManager') || e.affectsConfiguration('npm.scriptRunner')) { invalidateTasksCache(); if (treeDataProvider) { treeDataProvider.refresh(); diff --git a/extensions/package-lock.json b/extensions/package-lock.json index 2fe3d161698..e3e87e1f52d 100644 --- a/extensions/package-lock.json +++ b/extensions/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "typescript": "^5.7.3" + "typescript": "^5.8.2" }, "devDependencies": { "@parcel/watcher": "2.5.1", @@ -940,9 +940,9 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/extensions/package.json b/extensions/package.json index 73435185641..d24575b17f5 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^5.7.3" + "typescript": "^5.8.2" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/extensions/ruby/cgmanifest.json b/extensions/ruby/cgmanifest.json index 49719a8afe9..f54ddba7ce7 100644 --- a/extensions/ruby/cgmanifest.json +++ b/extensions/ruby/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "Shopify/ruby-lsp", "repositoryUrl": "https://github.com/Shopify/ruby-lsp", - "commitHash": "01a8ecf608b7d8607adcd89c32db72ae3852f33b" + "commitHash": "958bb1aa0c7aa4b6119c947b69afa7f12b19dceb" } }, "licenseDetail": [ diff --git a/extensions/ruby/syntaxes/ruby.tmLanguage.json b/extensions/ruby/syntaxes/ruby.tmLanguage.json index 84fb37228e6..785b0e4a928 100644 --- a/extensions/ruby/syntaxes/ruby.tmLanguage.json +++ b/extensions/ruby/syntaxes/ruby.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Shopify/ruby-lsp/commit/01a8ecf608b7d8607adcd89c32db72ae3852f33b", + "version": "https://github.com/Shopify/ruby-lsp/commit/958bb1aa0c7aa4b6119c947b69afa7f12b19dceb", "name": "Ruby", "scopeName": "source.ruby", "patterns": [ @@ -30,7 +30,7 @@ } }, "comment": "class Namespace::ClassName < OtherNamespace::OtherClassName", - "match": "\b(class)\\s+(([a-zA-Z0-9_]+)((::)[a-zA-Z0-9_]+)*)\\s*((<)\\s*(([a-zA-Z0-9_]+)((::)[a-zA-Z0-9_]+)*))?", + "match": "\\b(class)\\s+(([a-zA-Z0-9_]+)((::)[a-zA-Z0-9_]+)*)\\s*((<)\\s*(([a-zA-Z0-9_]+)((::)[a-zA-Z0-9_]+)*))?", "name": "meta.class.ruby" }, { @@ -45,7 +45,7 @@ "name": "punctuation.separator.namespace.ruby" } }, - "match": "\b(module)\\s+(([a-zA-Z0-9_]+)((::)[a-zA-Z0-9_]+)*)", + "match": "\\b(module)\\s+(([a-zA-Z0-9_]+)((::)[a-zA-Z0-9_]+)*)", "name": "meta.module.ruby" }, { @@ -57,7 +57,7 @@ "name": "punctuation.separator.inheritance.ruby" } }, - "match": "\b(class)\\s*(<<)\\s*", + "match": "\\b(class)\\s*(<<)\\s*", "name": "meta.class.ruby" }, { @@ -125,7 +125,7 @@ "name": "variable.ruby" } }, - "match": "^\\s*([a-z]([A-Za-z0-9_])*)\\s*=[^=>]", + "match": "^\\s*([a-z]([A-Za-z0-9_])*)\\s*(?==[^=>])", "comment": "A local variable assignment" }, { @@ -2211,7 +2211,7 @@ ] }, { - "begin": "(?<={|{\\s|[^A-Za-z0-9_:@$]do|^do|[^A-Za-z0-9_:@$]do\\s|^do\\s)(\\|)", + "begin": "(?<={|{\\s+|[^A-Za-z0-9_:@$]do|^do|[^A-Za-z0-9_:@$]do\\s+|^do\\s+)(\\|)", "name": "meta.block.parameters.ruby", "captures": { "1": { @@ -2225,16 +2225,13 @@ "end": "(?=,|\\|\\s*)", "patterns": [ { - "match": "\\G([&*]?)([a-zA-Z][\\w_]*)|(_[\\w_]*)", + "match": "\\G((?:&|\\*\\*?)?)([a-zA-Z_][\\w_]*)", "captures": { "1": { "name": "storage.type.variable.ruby" }, "2": { "name": "variable.other.block.ruby" - }, - "3": { - "name": "variable.other.block.unused.ruby variable.other.constant.ruby" } } } diff --git a/extensions/scss/cgmanifest.json b/extensions/scss/cgmanifest.json index 12247769ce2..a67a4f54609 100644 --- a/extensions/scss/cgmanifest.json +++ b/extensions/scss/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "atom/language-sass", "repositoryUrl": "https://github.com/atom/language-sass", - "commitHash": "f52ab12f7f9346cc2568129d8c4419bd3d506b47" + "commitHash": "303bbf0c250fe380b9e57375598cfd916110758b" } }, "license": "MIT", "description": "The file syntaxes/scss.json was derived from the Atom package https://github.com/atom/language-sass which was originally converted from the TextMate bundle https://github.com/alexsancho/SASS.tmbundle.", - "version": "0.62.1" + "version": "0.61.4" } ], "version": 1 diff --git a/extensions/sql/cgmanifest.json b/extensions/sql/cgmanifest.json index fec241862ef..4a1455ebd31 100644 --- a/extensions/sql/cgmanifest.json +++ b/extensions/sql/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "microsoft/vscode-mssql", "repositoryUrl": "https://github.com/microsoft/vscode-mssql", - "commitHash": "49eff02f68b6ee73025c6665c672ca1c93385dde" + "commitHash": "d07e0f838eabff968e4387841427d3c3af8aeec6" } }, "license": "MIT", - "version": "1.23.0" + "version": "1.29.0" } ], "version": 1 diff --git a/extensions/sql/syntaxes/sql.tmLanguage.json b/extensions/sql/syntaxes/sql.tmLanguage.json index 9db45221026..4341bc81528 100644 --- a/extensions/sql/syntaxes/sql.tmLanguage.json +++ b/extensions/sql/syntaxes/sql.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-mssql/commit/49eff02f68b6ee73025c6665c672ca1c93385dde", + "version": "https://github.com/microsoft/vscode-mssql/commit/d07e0f838eabff968e4387841427d3c3af8aeec6", "name": "SQL", "scopeName": "source.sql", "patterns": [ @@ -372,7 +372,7 @@ "include": "#regexps" }, { - "match": "\\b(?i)(abort|abort_after_wait|absent|absolute|accent_sensitivity|acceptable_cursopt|acp|action|activation|add|address|admin|aes_128|aes_192|aes_256|affinity|after|aggregate|algorithm|all_constraints|all_errormsgs|all_indexes|all_levels|all_results|allow_connections|allow_dup_row|allow_encrypted_value_modifications|allow_page_locks|allow_row_locks|allow_snapshot_isolation|alter|altercolumn|always|anonymous|ansi_defaults|ansi_null_default|ansi_null_dflt_off|ansi_null_dflt_on|ansi_nulls|ansi_padding|ansi_warnings|appdomain|append|application|apply|arithabort|arithignore|array|assembly|asymmetric|asynchronous_commit|at|atan2|atomic|attach|attach_force_rebuild_log|attach_rebuild_log|audit|auth_realm|authentication|auto|auto_cleanup|auto_close|auto_create_statistics|auto_drop|auto_shrink|auto_update_statistics|auto_update_statistics_async|automated_backup_preference|automatic|autopilot|availability|availability_mode|backup|backup_priority|base64|basic|batches|batchsize|before|between|bigint|binary|binding|bit|block|blockers|blocksize|bmk|both|break|broker|broker_instance|bucket_count|buffer|buffercount|bulk_logged|by|call|caller|card|case|catalog|catch|cert|certificate|change_retention|change_tracking|change_tracking_context|changes|char|character|character_set|check_expiration|check_policy|checkconstraints|checkindex|checkpoint|checksum|cleanup_policy|clear|clear_port|close|clustered|codepage|collection|column_encryption_key|column_master_key|columnstore|columnstore_archive|colv_80_to_100|colv_100_to_80|commit_differential_base|committed|compatibility_level|compress_all_row_groups|compression|compression_delay|concat_null_yields_null|concatenate|configuration|connect|connection|containment|continue|continue_after_error|contract|contract_name|control|conversation|conversation_group_id|conversation_handle|copy|copy_only|count_rows|counter|create(\\\\s+or\\\\s+alter)?|credential|cross|cryptographic|cryptographic_provider|cube|cursor|cursor_close_on_commit|cursor_default|data|data_compression|data_flush_interval_seconds|data_mirroring|data_purity|data_source|database|database_name|database_snapshot|datafiletype|date_correlation_optimization|date|datefirst|dateformat|date_format|datetime|datetime2|datetimeoffset|day(s)?|db_chaining|dbid|dbidexec|dbo_only|deadlock_priority|deallocate|dec|decimal|declare|decrypt|decrypt_a|decryption|default_database|default_fulltext_language|default_language|default_logon_domain|default_schema|definition|delay|delayed_durability|delimitedtext|density_vector|dependent|des|description|desired_state|desx|differential|digest|disable|disable_broker|disable_def_cnst_chk|disabled|disk|distinct|distributed|distribution|drop|drop_existing|dts_buffers|dump|durability|dynamic|edition|elements|else|emergency|empty|enable|enable_broker|enabled|encoding|encrypted|encrypted_value|encryption|encryption_type|end|endpoint|endpoint_url|enhancedintegrity|entry|error_broker_conversations|errorfile|estimateonly|event|except|exec|executable|execute|exists|expand|expiredate|expiry_date|explicit|external|external_access|failover|failover_mode|failure_condition_level|fast|fast_forward|fastfirstrow|federated_service_account|fetch|field_terminator|fieldterminator|file|filelistonly|filegroup|filegrowth|filename|filestream|filestream_log|filestream_on|filetable|file_format|filter|first_row|fips_flagger|fire_triggers|first|firstrow|float|flush_interval_seconds|fmtonly|following|for|force|force_failover_allow_data_loss|force_service_allow_data_loss|forced|forceplan|formatfile|format_options|format_type|formsof|forward_only|free_cursors|free_exec_context|fullscan|fulltext|fulltextall|fulltextkey|function|generated|get|geography|geometry|global|go|goto|governor|guid|hadoop|hardening|hash|hashed|header_limit|headeronly|health_check_timeout|hidden|hierarchyid|histogram|histogram_steps|hits_cursors|hits_exec_context|hour(s)?|http|identity|identity_value|if|ifnull|ignore|ignore_constraints|ignore_dup_key|ignore_dup_row|ignore_triggers|image|immediate|implicit_transactions|include|include_null_values|incremental|index|inflectional|init|initiator|insensitive|insert|instead|int|integer|integrated|intersect|intermediate|interval_length_minutes|into|inuse_cursors|inuse_exec_context|io|is|isabout|iso_week|isolation|job_tracker_location|json|keep|keep_nulls|keep_replication|keepdefaults|keepfixed|keepidentity|keepnulls|kerberos|key|key_path|key_source|key_store_provider_name|keyset|kill|kilobytes_per_batch|labelonly|langid|language|last|lastrow|leading|legacy_cardinality_estimation|length|level|lifetime|lineage_80_to_100|lineage_100_to_80|listener_ip|listener_port|load|loadhistory|lob_compaction|local|local_service_name|locate|location|lock_escalation|lock_timeout|lockres|log|login|login_type|loop|manual|mark_in_use_for_removal|masked|master|match|matched|max_queue_readers|max_duration|max_outstanding_io_per_volume|maxdop|maxerrors|maxlength|maxtransfersize|max_plans_per_query|max_storage_size_mb|mediadescription|medianame|mediapassword|memogroup|memory_optimized|merge|message|message_forward_size|message_forwarding|microsecond|millisecond|minute(s)?|mirror_address|misses_cursors|misses_exec_context|mixed|modify|money|month|move|multi_user|must_change|name|namespace|nanosecond|native|native_compilation|nchar|ncharacter|nested_triggers|never|new_account|new_broker|newname|next|no|no_browsetable|no_checksum|no_compression|no_infomsgs|no_triggers|no_truncate|nocount|noexec|noexpand|noformat|noinit|nolock|nonatomic|nonclustered|nondurable|none|norecompute|norecovery|noreset|norewind|noskip|not|notification|nounload|now|nowait|ntext|ntlm|nulls|numeric|numeric_roundabort|nvarchar|object|objid|oem|offline|old_account|online|operation_mode|open|openjson|optimistic|option|orc|out|outer|output|over|override|owner|ownership|pad_index|page|page_checksum|page_verify|pagecount|paglock|param|parameter_sniffing|parameter_type_expansion|parameterization|parquet|parseonly|partial|partition|partner|password|path|pause|percentage|permission_set|persisted|period|physical_only|plan_forcing_mode|policy|pool|population|ports|preceding|precision|predicate|presume_abort|primary|primary_role|print|prior|priority |priority_level|private|proc(edure)?|procedure_name|profile|provider|quarter|query_capture_mode|query_governor_cost_limit|query_optimizer_hotfixes|query_store|queue|quoted_identifier|raiserror|range|raw|rcfile|rc2|rc4|rc4_128|rdbms|read_committed_snapshot|read|read_only|read_write|readcommitted|readcommittedlock|readonly|readpast|readuncommitted|readwrite|real|rebuild|receive|recmodel_70backcomp|recompile|reconfigure|recovery|recursive|recursive_triggers|redo_queue|reject_sample_value|reject_type|reject_value|relative|remote|remote_data_archive|remote_proc_transactions|remote_service_name|remove|removed_cursors|removed_exec_context|reorganize|repeat|repeatable|repeatableread|replace|replica|replicated|replnick_100_to_80|replnickarray_80_to_100|replnickarray_100_to_80|required|required_cursopt|resample|reset|resource|resource_manager_location|respect|restart|restore|restricted_user|resume|retaindays|retention|return|revert|rewind|rewindonly|returns|robust|role|rollup|root|round_robin|route|row|rowdump|rowguidcol|rowlock|row_terminator|rows|rows_per_batch|rowsets_only|rowterminator|rowversion|rsa_1024|rsa_2048|rsa_3072|rsa_4096|rsa_512|safe|safety|sample|save|scalar|schema|schemabinding|scoped|scroll|scroll_locks|sddl|second|secexpr|seconds|secondary|secondary_only|secondary_role|secret|security|securityaudit|selective|self|send|sent|sequence|serde_method|serializable|server|service|service_broker|service_name|service_objective|session_timeout|session|sessions|seterror|setopts|sets|shard_map_manager|shard_map_name|sharded|shared_memory|shortest_path|show_statistics|showplan_all|showplan_text|showplan_xml|showplan_xml_with_recompile|shrinkdb|shutdown|sid|signature|simple|single_blob|single_clob|single_nclob|single_user|singleton|site|size|size_based_cleanup_mode|skip|smalldatetime|smallint|smallmoney|snapshot|snapshot_import|snapshotrestorephase|soap|softnuma|sort_in_tempdb|sorted_data|sorted_data_reorg|spatial|sql|sql_bigint|sql_binary|sql_bit|sql_char|sql_date|sql_decimal|sql_double|sql_float|sql_guid|sql_handle|sql_longvarbinary|sql_longvarchar|sql_numeric|sql_real|sql_smallint|sql_time|sql_timestamp|sql_tinyint|sql_tsi_day|sql_tsi_frac_second|sql_tsi_hour|sql_tsi_minute|sql_tsi_month|sql_tsi_quarter|sql_tsi_second|sql_tsi_week|sql_tsi_year|sql_type_date|sql_type_time|sql_type_timestamp|sql_varbinary|sql_varchar|sql_variant|sql_wchar|sql_wlongvarchar|ssl|ssl_port|standard|standby|start|start_date|started|stat_header|state|statement|static|statistics|statistics_incremental|statistics_norecompute|statistics_only|statman|stats|stats_stream|status|stop|stop_on_error|stopat|stopatmark|stopbeforemark|stoplist|stopped|string_delimiter|subject|supplemental_logging|supported|suspend|symmetric|synchronous_commit|synonym|sysname|system|system_time|system_versioning|table|tableresults|tablock|tablockx|take|tape|target|target_index|target_partition|target_recovery_time|tcp|temporal_history_retention|text|textimage_on|then|thesaurus|throw|time|timeout|timestamp|tinyint|to|top|torn_page_detection|track_columns_updated|trailing|tran|transaction|transfer|transform_noise_words|triple_des|triple_des_3key|truncate|trustworthy|try|tsql|two_digit_year_cutoff|type|type_desc|type_warning|tzoffset|uid|unbounded|uncommitted|unique|uniqueidentifier|unlimited|unload|unlock|unsafe|updlock|url|use|useplan|useroptions|use_type_default|using|utcdatetime|valid_xml|validation|value|values|varbinary|varchar|verbose|verifyonly|version|view_metadata|virtual_device|visiblity|wait_at_low_priority|waitfor|webmethod|week|weekday|weight|well_formed_xml|when|while|widechar|widechar_ansi|widenative|window|windows|with|within|within group|witness|without|without_array_wrapper|workload|wsdl|xact_abort|xlock|xml|xmlschema|xquery|xsinil|year|zone)\\b", + "match": "\\b(?i)(abort|abort_after_wait|absent|absolute|accent_sensitivity|acceptable_cursopt|acp|action|activation|add|address|admin|aes_128|aes_192|aes_256|affinity|after|aggregate|algorithm|all_constraints|all_errormsgs|all_indexes|all_levels|all_results|allow_connections|allow_dup_row|allow_encrypted_value_modifications|allow_page_locks|allow_row_locks|allow_snapshot_isolation|alter|altercolumn|always|anonymous|ansi_defaults|ansi_null_default|ansi_null_dflt_off|ansi_null_dflt_on|ansi_nulls|ansi_padding|ansi_warnings|appdomain|append|application|apply|arithabort|arithignore|array|assembly|asymmetric|asynchronous_commit|at|atan2|atomic|attach|attach_force_rebuild_log|attach_rebuild_log|audit|auth_realm|authentication|auto|auto_cleanup|auto_close|auto_create_statistics|auto_drop|auto_shrink|auto_update_statistics|auto_update_statistics_async|automated_backup_preference|automatic|autopilot|availability|availability_mode|backup|backup_priority|base64|basic|batches|batchsize|before|between|bigint|binary|binding|bit|block|blockers|blocksize|bmk|both|break|broker|broker_instance|bucket_count|buffer|buffercount|bulk_logged|by|call|caller|card|case|catalog|catch|cert|certificate|change_retention|change_tracking|change_tracking_context|changes|char|character|character_set|check_expiration|check_policy|checkconstraints|checkindex|checkpoint|checksum|cleanup_policy|clear|clear_port|close|clustered|codepage|collection|column_encryption_key|column_master_key|columnstore|columnstore_archive|colv_80_to_100|colv_100_to_80|commit_differential_base|committed|compatibility_level|compress_all_row_groups|compression|compression_delay|concat_null_yields_null|concatenate|configuration|connect|connection|containment|continue|continue_after_error|contract|contract_name|control|conversation|conversation_group_id|conversation_handle|copy|copy_only|count_rows|counter|create(\\\\s+or\\\\s+alter)?|credential|cross|cryptographic|cryptographic_provider|cube|cursor|cursor_close_on_commit|cursor_default|data|data_compression|data_flush_interval_seconds|data_mirroring|data_purity|data_source|database|database_name|database_snapshot|datafiletype|date_correlation_optimization|date|datefirst|dateformat|date_format|datetime|datetime2|datetimeoffset|day(s)?|db_chaining|dbid|dbidexec|dbo_only|deadlock_priority|deallocate|dec|decimal|declare|decrypt|decrypt_a|decryption|default_database|default_fulltext_language|default_language|default_logon_domain|default_schema|definition|delay|delayed_durability|delimitedtext|density_vector|dependent|des|description|desired_state|desx|differential|digest|disable|disable_broker|disable_def_cnst_chk|disabled|disk|distinct|distributed|distribution|drop|drop_existing|dts_buffers|dump|durability|dynamic|edition|elements|else|emergency|empty|enable|enable_broker|enabled|encoding|encrypted|encrypted_value|encryption|encryption_type|end|endpoint|endpoint_url|enhancedintegrity|entry|error_broker_conversations|errorfile|estimateonly|event|except|exec|executable|execute|exists|expand|expiredate|expiry_date|explicit|external|external_access|failover|failover_mode|failure_condition_level|fast|fast_forward|fastfirstrow|federated_service_account|fetch|field_terminator|fieldterminator|file|filelistonly|filegroup|filegrowth|filename|filestream|filestream_log|filestream_on|filetable|file_format|filter|first_row|fips_flagger|fire_triggers|first|firstrow|float|flush_interval_seconds|fmtonly|following|for|force|force_failover_allow_data_loss|force_service_allow_data_loss|forced|forceplan|formatfile|format_options|format_type|formsof|forward_only|free_cursors|free_exec_context|fullscan|fulltext|fulltextall|fulltextkey|function|generated|get|geography|geometry|global|go|goto|governor|guid|hadoop|hardening|hash|hashed|header_limit|headeronly|health_check_timeout|hidden|hierarchyid|histogram|histogram_steps|hits_cursors|hits_exec_context|hour(s)?|http|identity|identity_value|if|ifnull|ignore|ignore_constraints|ignore_dup_key|ignore_dup_row|ignore_triggers|image|immediate|implicit_transactions|include|include_null_values|incremental|index|inflectional|init|initiator|insensitive|insert|instead|int|integer|integrated|intersect|intermediate|interval_length_minutes|into|inuse_cursors|inuse_exec_context|io|is|isabout|iso_week|isolation|job_tracker_location|json|keep|keep_nulls|keep_replication|keepdefaults|keepfixed|keepidentity|keepnulls|kerberos|key|key_path|key_source|key_store_provider_name|keyset|kill|kilobytes_per_batch|labelonly|langid|language|last|lastrow|leading|legacy_cardinality_estimation|length|level|lifetime|lineage_80_to_100|lineage_100_to_80|listener_ip|listener_port|load|loadhistory|lob_compaction|local|local_service_name|locate|location|lock_escalation|lock_timeout|lockres|log|login|login_type|loop|manual|mark_in_use_for_removal|masked|master|match|matched|max_queue_readers|max_duration|max_outstanding_io_per_volume|maxdop|maxerrors|maxlength|maxtransfersize|max_plans_per_query|max_storage_size_mb|mediadescription|medianame|mediapassword|memogroup|memory_optimized|merge|message|message_forward_size|message_forwarding|microsecond|millisecond|minute(s)?|mirror_address|misses_cursors|misses_exec_context|mixed|modify|money|month|move|multi_user|must_change|name|namespace|nanosecond|native|native_compilation|nchar|ncharacter|nested_triggers|never|new_account|new_broker|newname|next|no|no_browsetable|no_checksum|no_compression|no_infomsgs|no_triggers|no_truncate|nocount|noexec|noexpand|noformat|noinit|nolock|nonatomic|nonclustered|nondurable|none|norecompute|norecovery|noreset|norewind|noskip|not|notification|nounload|now|nowait|ntext|ntlm|nulls|numeric|numeric_roundabort|nvarchar|object|objid|oem|offline|old_account|online|operation_mode|open|openjson|optimistic|option|orc|out|outer|output|over|override|owner|ownership|pad_index|page|page_checksum|page_verify|pagecount|paglock|param|parameter_sniffing|parameter_type_expansion|parameterization|parquet|parseonly|partial|partition|partner|password|path|pause|percentage|permission_set|persisted|period|physical_only|plan_forcing_mode|policy|pool|population|ports|preceding|precision|predicate|presume_abort|primary|primary_role|print|prior|priority |priority_level|private|proc(edure)?|procedure_name|profile|provider|quarter|query_capture_mode|query_governor_cost_limit|query_optimizer_hotfixes|query_store|queue|quoted_identifier|raiserror|range|raw|rcfile|rc2|rc4|rc4_128|rdbms|read_committed_snapshot|read|read_only|read_write|readcommitted|readcommittedlock|readonly|readpast|readuncommitted|readwrite|real|rebuild|receive|recmodel_70backcomp|recompile|reconfigure|recovery|recursive|recursive_triggers|redo_queue|reject_sample_value|reject_type|reject_value|relative|remote|remote_data_archive|remote_proc_transactions|remote_service_name|remove|removed_cursors|removed_exec_context|reorganize|repeat|repeatable|repeatableread|replace|replica|replicated|replnick_100_to_80|replnickarray_80_to_100|replnickarray_100_to_80|required|required_cursopt|resample|reset|resource|resource_manager_location|respect|restart|restore|restricted_user|resume|retaindays|retention|return|revert|rewind|rewindonly|returns|robust|role|rollup|root|round_robin|route|row|rowdump|rowguidcol|rowlock|row_terminator|rows|rows_per_batch|rowsets_only|rowterminator|rowversion|rsa_1024|rsa_2048|rsa_3072|rsa_4096|rsa_512|safe|safety|sample|save|scalar|schema|schemabinding|scoped|scroll|scroll_locks|sddl|second|secexpr|seconds|secondary|secondary_only|secondary_role|secret|security|securityaudit|selective|self|send|sent|sequence|serde_method|serializable|server|service|service_broker|service_name|service_objective|session_timeout|session|sessions|seterror|setopts|sets|shard_map_manager|shard_map_name|sharded|shared_memory|shortest_path|show_statistics|showplan_all|showplan_text|showplan_xml|showplan_xml_with_recompile|shrinkdb|shutdown|sid|signature|simple|single_blob|single_clob|single_nclob|single_user|singleton|site|size|size_based_cleanup_mode|skip|smalldatetime|smallint|smallmoney|snapshot|snapshot_import|snapshotrestorephase|soap|softnuma|sort_in_tempdb|sorted_data|sorted_data_reorg|spatial|sql|sql_bigint|sql_binary|sql_bit|sql_char|sql_date|sql_decimal|sql_double|sql_float|sql_guid|sql_handle|sql_longvarbinary|sql_longvarchar|sql_numeric|sql_real|sql_smallint|sql_time|sql_timestamp|sql_tinyint|sql_tsi_day|sql_tsi_frac_second|sql_tsi_hour|sql_tsi_minute|sql_tsi_month|sql_tsi_quarter|sql_tsi_second|sql_tsi_week|sql_tsi_year|sql_type_date|sql_type_time|sql_type_timestamp|sql_varbinary|sql_varchar|sql_variant|sql_wchar|sql_wlongvarchar|ssl|ssl_port|standard|standby|start|start_date|started|stat_header|state|statement|static|statistics|statistics_incremental|statistics_norecompute|statistics_only|statman|stats|stats_stream|status|stop|stop_on_error|stopat|stopatmark|stopbeforemark|stoplist|stopped|string_delimiter|subject|supplemental_logging|supported|suspend|symmetric|synchronous_commit|synonym|sysname|system|system_time|system_versioning|table|tableresults|tablock|tablockx|take|tape|target|target_index|target_partition|target_recovery_time|tcp|temporal_history_retention|text|textimage_on|then|thesaurus|throw|time|timeout|timestamp|tinyint|to|top|torn_page_detection|track_columns_updated|trailing|tran|transaction|transfer|transform_noise_words|triple_des|triple_des_3key|truncate|trustworthy|try|tsql|two_digit_year_cutoff|type|type_desc|type_warning|tzoffset|uid|unbounded|uncommitted|unique|uniqueidentifier|unlimited|unload|unlock|unsafe|updlock|url|use|useplan|useroptions|use_type_default|using|utcdatetime|valid_xml|validation|value|values|varbinary|varchar|vector|verbose|verifyonly|version|view_metadata|virtual_device|visiblity|wait_at_low_priority|waitfor|webmethod|week|weekday|weight|well_formed_xml|when|while|widechar|widechar_ansi|widenative|window|windows|with|within|within group|witness|without|without_array_wrapper|workload|wsdl|xact_abort|xlock|xml|xmlschema|xquery|xsinil|year|zone)\\b", "name": "keyword.other.sql" }, { diff --git a/extensions/terminal-suggest/ThirdPartyNotices.txt b/extensions/terminal-suggest/ThirdPartyNotices.txt index 761d5b01a42..f6de57f6266 100644 --- a/extensions/terminal-suggest/ThirdPartyNotices.txt +++ b/extensions/terminal-suggest/ThirdPartyNotices.txt @@ -28,5 +28,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - diff --git a/extensions/terminal-suggest/cgmanifest.json b/extensions/terminal-suggest/cgmanifest.json index f39b1fae371..ead3479e667 100644 --- a/extensions/terminal-suggest/cgmanifest.json +++ b/extensions/terminal-suggest/cgmanifest.json @@ -82,6 +82,63 @@ "SOFTWARE." ], "version": "1.1.2" + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/zsh-users/zsh", + "commitHash": "435cb1b748ce1f2f5c38edc1d64f4ee2424f9b3a" + } + }, + "version": "5.9", + "licenseDetail": [ + "Unless otherwise noted in the header of specific files, files in this distribution have the licence shown below.", + "", + "However, note that certain shell functions are licensed under versions of the GNU General Public Licence. Anyone distributing the shell as a binary including those files needs to take account of this. Search shell functions for \"Copyright\" for specific copyright information. None of the core functions are affected by this, so those files may simply be omitted.", + "", + "--", + "", + "The Z Shell is copyright (c) 1992-2017 Paul Falstad, Richard Coleman, Zoltán Hidvégi, Andrew Main, Peter Stephenson, Sven Wischnowsky, and others. All rights reserved. Individual authors, whether or not specifically named, retain copyright in all changes; in what follows, they are referred to as `the Zsh Development Group'. This is for convenience only and this body has no legal status. The Z shell is distributed under the following licence; any provisions made in individual files take precedence.", + "", + "Permission is hereby granted, without written agreement and without licence or royalty fees, to use, copy, modify, and distribute this software and to distribute modified versions of this software for any purpose, provided that the above copyright notice and the following two paragraphs appear in all copies of this software.", + "", + "In no event shall the Zsh Development Group be liable to any party for direct, indirect, special, incidental, or consequential damages arising out of the use of this software and its documentation, even if the Zsh Development Group have been advised of the possibility of such damage.", + "", + "The Zsh Development Group specifically disclaim any warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The software provided hereunder is on an \"as is\" basis, and the Zsh Development Group have no obligation to provide maintenance, support, updates, enhancements, or modifications." + ] + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/fish-shell/fish-shell", + "commitHash": "6627d403d33b4e74b49aa4db2a4f17709628cdc8" + } + }, + "version": "3.7.1", + "licenseDetail": [ + "Fish is a smart and user-friendly command line shell.", + "", + "Copyright (C) 2005-2009 Axel Liljencrantz", + "Copyright (C) 2009- fish-shell contributors", + "", + "fish is free software.", + "", + "Most of fish is licensed under the GNU General Public License version 2, and", + "you can redistribute it and/or modify it under the terms of the GNU GPL as", + "published by the Free Software Foundation.", + "", + "fish also includes software licensed under the Python Software Foundation License version 2, the MIT", + "license, and the GNU Library General Public License version 2.", + "", + "Full licensing information is contained in doc_src/license.rst.", + "", + "This program is distributed in the hope that it will be useful, but WITHOUT", + "ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or", + "FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for", + "more details." + ] } ], "version": 1 diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index dcc02bff6a2..f2e4104823c 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -21,7 +21,8 @@ "scripts": { "compile": "npx gulp compile-extension:terminal-suggest", "watch": "npx gulp watch-extension:terminal-suggest", - "pull-zshbuiltins": "ts-node ./scripts/pullZshBuiltins.ts" + "pull-zshbuiltins": "ts-node ./scripts/pullZshBuiltins.ts", + "pull-fishbuiltins": "ts-node ./scripts/pullFishBuiltins.ts" }, "main": "./out/terminalSuggestMain", "activationEvents": [ diff --git a/extensions/terminal-suggest/scripts/pullFishBuiltins.ts b/extensions/terminal-suggest/scripts/pullFishBuiltins.ts new file mode 100644 index 00000000000..8ac6b870d45 --- /dev/null +++ b/extensions/terminal-suggest/scripts/pullFishBuiltins.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { cleanupText, checkWindows, execAsync, copyright } from './terminalScriptHelpers'; + +checkWindows(); + +interface ICommandDetails { + description: string; + args: string | undefined; + shortDescription?: string; +} + +let fishBuiltinsCommandDescriptionsCache = new Map(); + +// Fallback descriptions for commands that don't return proper help information +const fallbackDescriptions: Record = { + '[': { + shortDescription: 'Test if a statement is true', + description: 'Evaluate an expression and return a status of true (0) or false (non-zero). Unlike the `test` command, the `[` command requires a closing `]`.', + args: 'EXPRESSION ]' + }, + 'break': { + shortDescription: 'Exit the current loop', + description: 'Terminate the execution of the nearest enclosing `while` or `for` loop and proceed with the next command after the loop.', + args: undefined + }, + 'breakpoint': { + shortDescription: 'Launch debug mode', + description: 'Pause execution and launch an interactive debug prompt. This is useful for inspecting the state of a script at a specific point.', + args: undefined + }, + 'case': { + shortDescription: 'Match a value against patterns', + description: 'Within a `switch` block, the `case` command specifies patterns to match against the given value, executing the associated block if a match is found.', + args: 'PATTERN...' + }, + 'continue': { + shortDescription: 'Skip to the next iteration of a loop', + description: 'Within a `while` or `for` loop, `continue` skips the remaining commands in the current iteration and proceeds to the next iteration of the loop.', + args: undefined + }, + 'else': { + shortDescription: 'Execute commands if the previous condition was false', + description: 'In an `if` block, the `else` section contains commands that execute if none of the preceding `if` or `else if` conditions were true.', + args: undefined + }, + 'end': { + shortDescription: 'Terminate a block of code', + description: 'Conclude a block of code initiated by constructs like `if`, `switch`, `while`, `for`, or `function`.', + args: undefined + }, + 'eval': { + shortDescription: 'Execute arguments as a command', + description: 'Concatenate all arguments into a single command and execute it. This allows for dynamic construction and execution of commands.', + args: 'COMMAND...' + }, + 'false': { + shortDescription: 'Return an unsuccessful result', + description: 'A command that returns a non-zero exit status, indicating failure. It is often used in scripts to represent a false condition.', + args: undefined + }, + 'realpath': { + shortDescription: 'Resolve and print the absolute path', + description: 'Convert each provided path to its absolute, canonical form by resolving symbolic links and relative path components.', + args: 'PATH...' + }, + ':': { + shortDescription: 'No operation command', + description: 'The `:` command is a no-op (no operation) command that returns a successful (zero) exit status. It can be used as a placeholder in scripts where a command is syntactically required but no action is desired.', + args: undefined + }, + 'test': { + shortDescription: 'Evaluate conditional expressions', + description: 'The `test` command evaluates conditional expressions and sets the exit status to 0 if the expression is true, and 1 if it is false. It supports various operators to evaluate expressions related to strings, numbers, and file attributes.', + args: 'EXPRESSION' + }, + 'true': { + shortDescription: 'Return a successful result', + description: 'The `true` command always returns a successful (zero) exit status. It is often used in scripts and conditional statements where an unconditional success result is needed.', + args: undefined + }, + 'printf': { + shortDescription: 'Display formatted text', + description: 'The `printf` command formats and prints text according to a specified format string. Unlike `echo`, `printf` does not append a newline unless explicitly included in the format.', + args: 'FORMAT [ARGUMENT...]' + } +}; + + +async function createCommandDescriptionsCache(): Promise { + const cachedCommandDescriptions: Map = new Map(); + + try { + // Get list of all builtins + const builtinsOutput = await execAsync('fish -c "builtin -n"').then(r => r.stdout.trim()); + const builtins = builtinsOutput.split('\n'); + + console.log(`Found ${builtins.length} Fish builtin commands`); + + for (const cmd of builtins) { + try { + // Get help info for each builtin + const helpOutput = await execAsync(`fish -c "${cmd} --help 2>&1"`).then(r => r.stdout); + let set = false; + if (helpOutput && !helpOutput.includes('No help for function') && !helpOutput.includes('See the web documentation')) { + const cleanHelpText = cleanupText(helpOutput); + + // Split the text into lines to process + const lines = cleanHelpText.split('\n'); + + + // Extract the short description, args, and full description + const { shortDescription, args, description } = extractHelpContent(cmd, lines); + + cachedCommandDescriptions.set(cmd, { + shortDescription, + description, + args + }); + set = description !== ''; + } + if (!set) { + // Use fallback descriptions for commands that don't return proper help + if (fallbackDescriptions[cmd]) { + console.info(`Using fallback description for ${cmd}`); + cachedCommandDescriptions.set(cmd, fallbackDescriptions[cmd]); + } else { + console.info(`No fallback description exists for ${cmd}`); + } + } + } catch { + // Use fallback descriptions for commands that throw an error + if (fallbackDescriptions[cmd]) { + console.info('Using fallback description for', cmd); + cachedCommandDescriptions.set(cmd, fallbackDescriptions[cmd]); + } else { + console.info(`Error getting help for ${cmd}`); + } + } + } + } catch (e) { + console.error('Error creating Fish builtins cache:', e); + process.exit(1); + } + + fishBuiltinsCommandDescriptionsCache = cachedCommandDescriptions; +} + +/** + * Extracts short description, args, and full description from help text lines + */ +function extractHelpContent(cmd: string, lines: string[]): { shortDescription: string; args: string | undefined; description: string } { + let shortDescription = ''; + let args: string | undefined; + let description = ''; + + // Skip the first line (usually just command name and basic usage) + let i = 1; + + // Skip any leading empty lines + while (i < lines.length && lines[i].trim().length === 0) { + i++; + } + + // The next non-empty line after the command name is typically + // either the short description or additional usage info + const startLine = i; + + // Find where the short description starts + if (i < lines.length) { + // First, check if the line has a command prefix and remove it + let firstContentLine = lines[i].trim(); + const cmdPrefixRegex = new RegExp(`^${cmd}\\s*-\\s*`, 'i'); + firstContentLine = firstContentLine.replace(cmdPrefixRegex, ''); + + // First non-empty line is the short description + shortDescription = firstContentLine; + i++; + + // Next non-empty line (after short description) is typically args + while (i < lines.length && lines[i].trim().length === 0) { + i++; + } + + if (i < lines.length) { + // Found a line after the short description - that's our args + args = lines[i].trim(); + i++; + } + } + + // Find the DESCRIPTION marker which marks the end of args section + let descriptionIndex = -1; + for (let j = i; j < lines.length; j++) { + if (lines[j].trim() === 'DESCRIPTION') { + descriptionIndex = j; + break; + } + } + + // If DESCRIPTION marker is found, consider everything between i and descriptionIndex as part of args + if (descriptionIndex > i) { + // Combine lines from i up to (but not including) descriptionIndex + const additionalArgs = lines.slice(i, descriptionIndex).join('\n').trim(); + if (additionalArgs) { + args = args ? `${args}\n${additionalArgs}` : additionalArgs; + } + i = descriptionIndex + 1; // Move past the DESCRIPTION line + } + + // The rest is the full description (skipping any empty lines after args) + while (i < lines.length && lines[i].trim().length === 0) { + i++; + } + + // Combine the remaining lines into the full description + description = lines.slice(Math.max(i, startLine)).join('\n').trim(); + + // If description is empty, use the short description + if (!description && shortDescription) { + description = shortDescription; + } + + // Extract just the first sentence for short description + const firstPeriodIndex = shortDescription.indexOf('.'); + if (firstPeriodIndex > 0) { + shortDescription = shortDescription.substring(0, firstPeriodIndex + 1).trim(); + } else if (shortDescription.length > 100) { + shortDescription = shortDescription.substring(0, 100) + '...'; + } + + return { + shortDescription, + args, + description + }; +} + +const main = async () => { + try { + await createCommandDescriptionsCache(); + console.log('Created Fish command descriptions cache with', fishBuiltinsCommandDescriptionsCache.size, 'entries'); + + // Save the cache to a TypeScript file + const cacheFilePath = path.join(__dirname, '../src/shell/fishBuiltinsCache.ts'); + const cacheObject = Object.fromEntries(fishBuiltinsCommandDescriptionsCache); + const tsContent = `${copyright}\n\nexport const fishBuiltinsCommandDescriptionsCache = ${JSON.stringify(cacheObject, null, 2)} as const;`; + await fs.writeFile(cacheFilePath, tsContent, 'utf8'); + console.log('Saved Fish command descriptions cache to fishBuiltinsCache.ts with', Object.keys(cacheObject).length, 'entries'); + } catch (error) { + console.error('Error:', error); + } +}; + +main(); diff --git a/extensions/terminal-suggest/scripts/pullZshBuiltins.ts b/extensions/terminal-suggest/scripts/pullZshBuiltins.ts index 30878aa1154..a05866b16bc 100644 --- a/extensions/terminal-suggest/scripts/pullZshBuiltins.ts +++ b/extensions/terminal-suggest/scripts/pullZshBuiltins.ts @@ -3,16 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { exec } from 'child_process'; -import { promisify } from 'util'; import * as fs from 'fs/promises'; import * as path from 'path'; -import { platform } from 'os'; +import { checkWindows, execAsync, copyright } from './terminalScriptHelpers'; -if (platform() === 'win32') { - console.error('\x1b[31mThis command is not supported on Windows\x1b[0m'); - process.exit(1); -} +checkWindows(); const latestZshVersion = 5.9; @@ -126,14 +121,14 @@ const shortDescriptions: Map = new Map([ ['ztcp', 'Manipulate TCP sockets'], ]); -const execAsync = promisify(exec); - interface ICommandDetails { description: string; args: string | undefined; shortDescription?: string; } + let zshBuiltinsCommandDescriptionsCache = new Map(); + async function createCommandDescriptionsCache(): Promise { const cachedCommandDescriptions: Map = new Map(); let output = ''; @@ -248,11 +243,12 @@ const main = async () => { console.log('\x1b[31mmissing short description for commands:\n' + missingShortDescription.join('\n') + '\x1b[0m'); } - // Save the cache to a JSON file - const cacheFilePath = path.join(__dirname, '../src/shell/zshBuiltinsCache.json'); + // Save the cache to a TypeScript file + const cacheFilePath = path.join(__dirname, '../src/shell/zshBuiltinsCache.ts'); const cacheObject = Object.fromEntries(zshBuiltinsCommandDescriptionsCache); - await fs.writeFile(cacheFilePath, JSON.stringify(cacheObject, null, 2), 'utf8'); - console.log('saved command descriptions cache to zshBuiltinsCache.json with ', Object.keys(cacheObject).length, 'entries'); + const tsContent = `${copyright}\n\nexport const zshBuiltinsCommandDescriptionsCache = ${JSON.stringify(cacheObject, null, 2)} as const;`; + await fs.writeFile(cacheFilePath, tsContent, 'utf8'); + console.log('saved command descriptions cache to zshBuiltinsCache.ts with ', Object.keys(cacheObject).length, 'entries'); } catch (error) { console.error('Error:', error); } diff --git a/extensions/terminal-suggest/scripts/terminalScriptHelpers.ts b/extensions/terminal-suggest/scripts/terminalScriptHelpers.ts new file mode 100644 index 00000000000..402e6d5d288 --- /dev/null +++ b/extensions/terminal-suggest/scripts/terminalScriptHelpers.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { platform } from 'os'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +export const execAsync = promisify(exec); + +/** + * Cleans up text from terminal control sequences and formatting artifacts + */ +export function cleanupText(text: string): string { + // Remove ANSI escape codes + let cleanedText = text.replace(/\x1b\[\d+m/g, ''); + + // Remove backspace sequences (like a\bb which tries to print a, move back, print b) + // This regex looks for a character followed by a backspace and another character + const backspaceRegex = /.\x08./g; + while (backspaceRegex.test(cleanedText)) { + cleanedText = cleanedText.replace(backspaceRegex, match => match.charAt(2)); + } + + // Remove any remaining backspaces and their preceding characters + cleanedText = cleanedText.replace(/.\x08/g, ''); + + // Remove underscores that are used for formatting in some fish help output + cleanedText = cleanedText.replace(/_\b/g, ''); + + return cleanedText; +} + +/** + * Copyright notice for generated files + */ +export const copyright = `/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/`; + +/** + * Checks if the script is running on Windows and exits if so + */ +export function checkWindows(): void { + if (platform() === 'win32') { + console.error('\x1b[31mThis command is not supported on Windows\x1b[0m'); + process.exit(1); + } +} diff --git a/extensions/terminal-suggest/scripts/update-specs.js b/extensions/terminal-suggest/scripts/update-specs.js index 5f6e0ec5717..775c5dd3264 100644 --- a/extensions/terminal-suggest/scripts/update-specs.js +++ b/extensions/terminal-suggest/scripts/update-specs.js @@ -14,8 +14,25 @@ const replaceStrings = [ [ 'import { filepaths } from "@fig/autocomplete-generators";', 'import { filepaths } from \'../../helpers/filepaths\';' - ] -] + ], +]; +const indentSearch = [20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1].map(e => new RegExp('^' + ' '.repeat(e * 2), 'gm')); +const indentReplaceValue = [20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1].map(e => '\t'.repeat(e)); + +const specSpecificReplaceStrings = new Map([ + ['git', [ + [ + 'import { ai } from "@fig/autocomplete-generators";', + 'function ai(...args: any[]): undefined { return undefined; }' + ], [ + 'prompt: async ({ executeCommand }) => {', + 'prompt: async ({ executeCommand }: any) => {' + ], [ + 'message: async ({ executeCommand }) =>', + 'message: async ({ executeCommand }: any) =>' + ] + ]], +]); for (const spec of upstreamSpecs) { const source = path.join(extRoot, `third_party/autocomplete/src/${spec}.ts`); @@ -26,5 +43,15 @@ for (const spec of upstreamSpecs) { for (const replaceString of replaceStrings) { content = content.replaceAll(replaceString[0], replaceString[1]); } + for (let i = 0; i < indentSearch.length; i++) { + content = content.replaceAll(indentSearch[i], indentReplaceValue[i]); + } + const thisSpecReplaceStrings = specSpecificReplaceStrings.get(spec); + if (thisSpecReplaceStrings) { + for (const replaceString of thisSpecReplaceStrings) { + content = content.replaceAll(replaceString[0], replaceString[1]); + } + } + fs.writeFileSync(destination, content); } diff --git a/extensions/terminal-suggest/src/completions/code-insiders.ts b/extensions/terminal-suggest/src/completions/code-insiders.ts index 89d01dc536f..1eb4f04acb1 100644 --- a/extensions/terminal-suggest/src/completions/code-insiders.ts +++ b/extensions/terminal-suggest/src/completions/code-insiders.ts @@ -2,12 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import code from './code'; +import code, { commonOptions, extensionManagementOptions, troubleshootingOptions } from './code'; const codeInsidersCompletionSpec: Fig.Spec = { ...code, name: 'code-insiders', description: 'Visual Studio Code Insiders', + options: [ + ...commonOptions, + ...extensionManagementOptions('code-insiders'), + ...troubleshootingOptions('code-insiders'), + ], }; export default codeInsidersCompletionSpec; diff --git a/extensions/terminal-suggest/src/completions/code.ts b/extensions/terminal-suggest/src/completions/code.ts index c514a32e2e1..919253c387f 100644 --- a/extensions/terminal-suggest/src/completions/code.ts +++ b/extensions/terminal-suggest/src/completions/code.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const commonOptions: Fig.Option[] = [ +import { filepaths } from '../helpers/filepaths'; + +export const commonOptions: Fig.Option[] = [ { name: '-', description: `Read from stdin (e.g. 'ps aux | grep code | code -')`, @@ -60,7 +62,6 @@ const commonOptions: Fig.Option[] = [ 'Open a file at the path on the specified line and character position', args: { name: 'file:line[:character]', - // TODO: Support :line[:character] completion? template: 'filepaths', }, }, @@ -135,9 +136,25 @@ const commonOptions: Fig.Option[] = [ name: ['-h', '--help'], description: 'Print usage', }, + { + name: '--locate-shell-integration-path', + description: + 'Print the path to the shell integration script for the provided shell', + args: { + isOptional: false, + name: 'shell', + description: 'The shell to locate the integration script for', + suggestions: [ + 'bash', + 'fish', + 'pwsh', + 'zsh', + ] + } + } ]; -const extensionManagementOptions: Fig.Option[] = [ +export const extensionManagementOptions = (cliName: string): Fig.Option[] => [ { name: '--extensions-dir', description: 'Set the root path for extensions', @@ -188,8 +205,13 @@ const extensionManagementOptions: Fig.Option[] = [ description: `Installs or updates an extension. The argument is either an extension id or a path to a VSIX. The identifier of an extension is '\${ publisher }.\${ name }'. Use '--force' argument to update to latest version. To install a specific version provide '@\${version}'. For example: 'vscode.csharp@1.2.3'`, args: { - // TODO: Create extension ID generator name: 'extension-id[@version] | path-to-vsix', + generators: [ + createCodeGenerators(cliName), + filepaths({ + extensions: ['vsix'], + }), + ], }, }, { @@ -201,8 +223,8 @@ const extensionManagementOptions: Fig.Option[] = [ name: '--uninstall-extension', description: 'Uninstalls an extension', args: { - // TODO: Create extension ID generator name: 'extension-id', + generators: createCodeGenerators(cliName) }, }, { @@ -212,7 +234,7 @@ const extensionManagementOptions: Fig.Option[] = [ }, ]; -const troubleshootingOptions: Fig.Option[] = [ +export const troubleshootingOptions = (cliName: string): Fig.Option[] => [ { name: ['-v', '--version'], description: 'Print version', @@ -254,8 +276,8 @@ const troubleshootingOptions: Fig.Option[] = [ name: '--disable-extension', description: 'Disable an extension', args: { - // TODO: Create extension ID generator name: 'extension-id', + generators: createCodeGenerators(cliName) }, }, { @@ -301,6 +323,26 @@ const troubleshootingOptions: Fig.Option[] = [ }, ]; +export function createCodeGenerators(cliName: string): Fig.Generator { + return { + script: [cliName, '--list-extensions', '--show-versions'], + postProcess: parseInstalledExtensions + }; +} + +export function parseInstalledExtensions(out: string): Fig.Suggestion[] | undefined { + const extensions = out.split('\n').filter(Boolean).map((line) => { + const [id, version] = line.split('@'); + return { + name: id, + type: 'option' as Fig.SuggestionType, + description: `Version: ${version}` + }; + }); + return extensions; +} + + const codeCompletionSpec: Fig.Spec = { name: 'code', description: 'Visual Studio Code', @@ -310,9 +352,10 @@ const codeCompletionSpec: Fig.Spec = { }, options: [ ...commonOptions, - ...extensionManagementOptions, - ...troubleshootingOptions, + ...extensionManagementOptions('code'), + ...troubleshootingOptions('code'), ], }; export default codeCompletionSpec; + diff --git a/extensions/terminal-suggest/src/completions/npx.ts b/extensions/terminal-suggest/src/completions/npx.ts new file mode 100644 index 00000000000..360103911d6 --- /dev/null +++ b/extensions/terminal-suggest/src/completions/npx.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const completionSpec: Fig.Spec = { + name: 'npx', + description: 'Execute binaries from npm packages', + args: { + name: 'command', + isCommand: true, + generators: { + script: [ + 'bash', + '-c', + 'until [[ -d node_modules/ ]] || [[ $PWD = \'/\' ]]; do cd ..; done; ls -1 node_modules/.bin/', + ], + postProcess: function (out) { + return out + .split('\n') + .map((name) => ({ + name, + icon: 'fig://icon?type=command', + loadSpec: name, + })); + }, + }, + isOptional: true, + }, + + options: [ + { + name: ['--package', '-p'], + description: 'Package to be installed', + args: { + name: 'package', + }, + }, + { + name: '--cache', + args: { + name: 'path', + template: 'filepaths', + }, + description: 'Location of the npm cache', + }, + { + name: '--always-spawn', + description: 'Always spawn a child process to execute the command', + }, + { + name: '-y', + description: 'Execute npx command without prompting for confirmation', + }, + { + description: 'Skip installation if a package is missing', + name: '--no-install', + }, + { + args: { + name: 'path', + template: 'filepaths', + }, + description: 'Path to user npmrc', + name: '--userconfig', + }, + { + name: ['--call', '-c'], + args: { + name: 'script', + }, + description: 'Execute string as if inside `npm run-script`', + }, + { + name: ['--shell', '-s'], + description: 'Shell to execute the command with, if any', + args: { + name: 'shell', + suggestions: [ + { + name: 'bash', + }, + { + name: 'fish', + }, + { + name: 'zsh', + }, + ], + }, + }, + { + args: { + name: 'shell-fallback', + suggestions: [ + { + name: 'bash', + }, + { + name: 'fish', + }, + { + name: 'zsh', + }, + ], + }, + name: '--shell-auto-fallback', + description: + 'Generate shell code to use npx as the "command not found" fallback', + }, + { + name: '--ignore-existing', + description: + 'Ignores existing binaries in $PATH, or in the localproject. This forces npx to do a temporary install and use the latest version', + }, + { + name: ['--quiet', '-q'], + description: + 'Suppress output from npx itself. Subcommands will not be affected', + }, + { + name: '--npm', + args: { + name: 'path to binary', + template: 'filepaths', + }, + description: 'Npm binary to use for internal operations', + }, + { + args: {}, + description: 'Extra node argument when calling a node binary', + name: ['--node-arg', '-n'], + }, + { + description: 'Show version number', + name: ['--version', '-v'], + }, + { + description: 'Show help', + name: ['--help', '-h'], + }, + ], +}; + +export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/apt.ts b/extensions/terminal-suggest/src/completions/upstream/apt.ts index e3ebbb89a0d..2d44332b492 100644 --- a/extensions/terminal-suggest/src/completions/upstream/apt.ts +++ b/extensions/terminal-suggest/src/completions/upstream/apt.ts @@ -134,10 +134,7 @@ const completionSpec: Fig.Spec = { name: "package", description: "The package you want to install", isVariadic: true, - generators: [ - packages, - filepaths({ extensions: ["deb"] }) - ], + generators: [packages, filepaths({ extensions: ["deb"] })], }, options: [ ...installationOptions, diff --git a/extensions/terminal-suggest/src/completions/upstream/brew.ts b/extensions/terminal-suggest/src/completions/upstream/brew.ts index 99b87c45ca9..4e0f0cbe2bb 100644 --- a/extensions/terminal-suggest/src/completions/upstream/brew.ts +++ b/extensions/terminal-suggest/src/completions/upstream/brew.ts @@ -1,1675 +1,1675 @@ const servicesGenerator = (action: string): Fig.Generator => ({ - script: ["bash", "-c", "brew services list | sed -e 's/ .*//' | tail -n +2"], - postProcess: function (out) { - return out - .split("\n") - .filter((line) => !line.includes("unbound")) - .map((line) => ({ - name: line, - icon: "fig://icon?type=package", - description: `${action} ${line}`, - })); - }, + script: ["bash", "-c", "brew services list | sed -e 's/ .*//' | tail -n +2"], + postProcess: function (out) { + return out + .split("\n") + .filter((line) => !line.includes("unbound")) + .map((line) => ({ + name: line, + icon: "fig://icon?type=package", + description: `${action} ${line}`, + })); + }, }); const repositoriesGenerator = (): Fig.Generator => ({ - script: ["brew", "tap"], - postProcess: (out) => { - return out.split("\n").map((line) => ({ name: line })); - }, + script: ["brew", "tap"], + postProcess: (out) => { + return out.split("\n").map((line) => ({ name: line })); + }, }); const formulaeGenerator: Fig.Generator = { - script: ["brew", "list", "-1"], - postProcess: function (out) { - return out - .split("\n") - .filter((line) => !line.includes("=")) - .map((formula) => ({ - name: formula, - icon: "🍺", - description: "Installed formula", - })); - }, + script: ["brew", "list", "-1"], + postProcess: function (out) { + return out + .split("\n") + .filter((line) => !line.includes("=")) + .map((formula) => ({ + name: formula, + icon: "🍺", + description: "Installed formula", + })); + }, }; const outdatedformulaeGenerator: Fig.Generator = { - script: ["brew", "outdated", "-q"], - postProcess: function (out) { - return out.split("\n").map((formula) => ({ - name: formula, - icon: "🍺", - description: "Outdated formula", - })); - }, + script: ["brew", "outdated", "-q"], + postProcess: function (out) { + return out.split("\n").map((formula) => ({ + name: formula, + icon: "🍺", + description: "Outdated formula", + })); + }, }; const generateAllFormulae: Fig.Generator = { - script: ["brew", "formulae"], - postProcess: function (out) { - return out.split("\n").map((formula) => ({ - name: formula, - icon: "🍺", - description: "Formula", - priority: 51, - })); - }, + script: ["brew", "formulae"], + postProcess: function (out) { + return out.split("\n").map((formula) => ({ + name: formula, + icon: "🍺", + description: "Formula", + priority: 51, + })); + }, }; const generateAllCasks: Fig.Generator = { - script: ["brew", "casks"], - postProcess: function (out) { - return out.split("\n").map((cask) => ({ - name: cask, - icon: "🍺", - description: "Cask", - priority: 52, - })); - }, + script: ["brew", "casks"], + postProcess: function (out) { + return out.split("\n").map((cask) => ({ + name: cask, + icon: "🍺", + description: "Cask", + priority: 52, + })); + }, }; const generateAliases: Fig.Generator = { - script: [ - "bash", - "-c", - 'find ~/.brew-aliases/ -type f ! -name "*.*" -d 1 | sed "s/.*\\///"', - ], - postProcess: function (out) { - return out - .split("\n") - .filter((line) => line && line.trim() !== "") - .map((line) => ({ - name: line, - icon: "fig://icon?type=command", - description: `Execute alias ${line}`, - })); - }, + script: [ + "bash", + "-c", + 'find ~/.brew-aliases/ -type f ! -name "*.*" -d 1 | sed "s/.*\\///"', + ], + postProcess: function (out) { + return out + .split("\n") + .filter((line) => line && line.trim() !== "") + .map((line) => ({ + name: line, + icon: "fig://icon?type=command", + description: `Execute alias ${line}`, + })); + }, }; const commonOptions: Fig.Option[] = [ - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { - name: ["-v", "--verbose"], - description: "Make some output more verbose", - }, - { name: ["-h", "--help"], description: "Show help message" }, + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { + name: ["-v", "--verbose"], + description: "Make some output more verbose", + }, + { name: ["-h", "--help"], description: "Show help message" }, ]; const completionSpec: Fig.Spec = { - name: "brew", - description: "Package manager for macOS", - subcommands: [ - { - name: "list", - description: "List all installed formulae", - options: [ - ...commonOptions, - { - name: ["--formula", "--formulae"], - description: - "List only formulae, or treat all named arguments as formulae", - }, - { - name: ["--cask", "--casks"], - description: "List only casks, or treat all named arguments as casks", - }, - { - name: "--unbrewed", - description: - "List files in Homebrew's prefix not installed by Homebrew. (disabled; replaced by brew --prefix --unbrewed)", - }, - { - name: "--full-name", - description: - "Print formulae with fully-qualified names. Unless --full-name, --versions or", - }, - { - name: "--pinned", - description: - "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", - }, - { - name: "--versions", - description: - "Show the version number for installed formulae, or only the specified formulae if formula are provided", - }, - { - name: "--multiple", - description: "Only show formulae with multiple versions installed", - }, - { - name: "--pinned", - description: - "List only pinned formulae, or only the specified (pinned) formulae if formula are provided. See also pin, unpin", - }, - { - name: "-1", - description: - "Force output to be one entry per line. This is the default when output is not to a terminal", - }, - { - name: "-l", - description: - "List formulae and/or casks in long format. Has no effect when a formula or cask name is passed as an argument", - }, - { - name: "-r", - description: - "Reverse the order of the formulae and/or casks sort to list the oldest entries first. Has no effect when a formula or cask name is passed as an argument", - }, - { - name: "-t", - description: - "Sort formulae and/or casks by time modified, listing most recently modified first. Has no effect when a formula or cask name is passed as an argument", - }, - ], - args: { - isOptional: true, - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: "ls", - description: "List all installed formulae", - options: [ - ...commonOptions, - { - name: "--formula", - description: - "List only formulae, or treat all named arguments as formulae", - }, - { - name: "--cask", - description: "List only casks, or treat all named arguments as casks", - }, - { - name: "--unbrewed", - description: - "List files in Homebrew's prefix not installed by Homebrew. (disabled; replaced by brew --prefix --unbrewed)", - }, - { - name: "--full-name", - description: - "Print formulae with fully-qualified names. Unless --full-name, --versions or", - }, - { - name: "--pinned", - description: - "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", - }, - { - name: "--versions", - description: - "Show the version number for installed formulae, or only the specified formulae if formula are provided", - }, - { - name: "--multiple", - description: "Only show formulae with multiple versions installed", - }, - { - name: "--pinned", - description: - "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", - }, - { - name: "-1", - description: - "Force output to be one entry per line. This is the default when output is not to a terminal", - }, - { - name: "-l", - description: - "List formulae and/or casks in long format. Has no effect when a formula or cask name is passed as an argument", - }, - { - name: "-r", - description: - "Reverse the order of the formulae and/or casks sort to list the oldest entries first. Has no effect when a formula or cask name is passed as an argument", - }, - { - name: "-t", - description: - "Sort formulae and/or casks by time modified, listing most recently modified first. Has no effect when a formula or cask name is passed as an argument", - }, - ], - args: { - isOptional: true, - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: "leaves", - description: - "List installed formulae that are not dependencies of another installed formula", - options: [ - { - name: ["-r", "--installed-on-request"], - description: "Show manually installed formula", - }, - { - name: ["-p", "--installed-as-dependency"], - description: "Show installed formula as dependencies", - }, - ], - }, - { - name: "doctor", - description: "Check your system for potential problems", - options: [ - ...commonOptions, - { - name: "--list-checks", - description: "List all audit methods", - }, - { - name: ["-D", "--audit-debug"], - description: "Enable debugging and profiling of audit methods", - }, - ], - }, - { - name: ["abv", "info"], - description: "Display brief statistics for your Homebrew installation", - args: { - isVariadic: true, - isOptional: true, - name: "formula", - description: "Formula or cask to summarize", - generators: [generateAllFormulae, generateAllCasks], - }, - options: [ - { - name: ["--cask", "--casks"], - description: "List only casks, or treat all named arguments as casks", - }, - { - name: "--analytics", - description: - "List global Homebrew analytics data or, if specified, installation and build error data for formula", - }, - { - name: "--days", - description: "How many days of analytics data to retrieve", - exclusiveOn: ["--analytics"], - args: { - name: "days", - description: "Number of days of data to retrieve", - suggestions: ["30", "90", "365"], - }, - }, - { - name: "--category", - description: "Which type of analytics data to retrieve", - exclusiveOn: ["--analytics"], - args: { - generators: { - custom: async (ctx) => { - // if anything provided after the subcommand does not begin with '-' - // then a formula has been provided and we should provide info on it - if ( - ctx.slice(2, ctx.length - 1).some((token) => token[0] !== "-") - ) { - return ["install", "install-on-request", "build-error"].map( - (sugg) => ({ - name: sugg, - }) - ); - } + name: "brew", + description: "Package manager for macOS", + subcommands: [ + { + name: "list", + description: "List all installed formulae", + options: [ + ...commonOptions, + { + name: ["--formula", "--formulae"], + description: + "List only formulae, or treat all named arguments as formulae", + }, + { + name: ["--cask", "--casks"], + description: "List only casks, or treat all named arguments as casks", + }, + { + name: "--unbrewed", + description: + "List files in Homebrew's prefix not installed by Homebrew. (disabled; replaced by brew --prefix --unbrewed)", + }, + { + name: "--full-name", + description: + "Print formulae with fully-qualified names. Unless --full-name, --versions or", + }, + { + name: "--pinned", + description: + "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", + }, + { + name: "--versions", + description: + "Show the version number for installed formulae, or only the specified formulae if formula are provided", + }, + { + name: "--multiple", + description: "Only show formulae with multiple versions installed", + }, + { + name: "--pinned", + description: + "List only pinned formulae, or only the specified (pinned) formulae if formula are provided. See also pin, unpin", + }, + { + name: "-1", + description: + "Force output to be one entry per line. This is the default when output is not to a terminal", + }, + { + name: "-l", + description: + "List formulae and/or casks in long format. Has no effect when a formula or cask name is passed as an argument", + }, + { + name: "-r", + description: + "Reverse the order of the formulae and/or casks sort to list the oldest entries first. Has no effect when a formula or cask name is passed as an argument", + }, + { + name: "-t", + description: + "Sort formulae and/or casks by time modified, listing most recently modified first. Has no effect when a formula or cask name is passed as an argument", + }, + ], + args: { + isOptional: true, + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: "ls", + description: "List all installed formulae", + options: [ + ...commonOptions, + { + name: "--formula", + description: + "List only formulae, or treat all named arguments as formulae", + }, + { + name: "--cask", + description: "List only casks, or treat all named arguments as casks", + }, + { + name: "--unbrewed", + description: + "List files in Homebrew's prefix not installed by Homebrew. (disabled; replaced by brew --prefix --unbrewed)", + }, + { + name: "--full-name", + description: + "Print formulae with fully-qualified names. Unless --full-name, --versions or", + }, + { + name: "--pinned", + description: + "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", + }, + { + name: "--versions", + description: + "Show the version number for installed formulae, or only the specified formulae if formula are provided", + }, + { + name: "--multiple", + description: "Only show formulae with multiple versions installed", + }, + { + name: "--pinned", + description: + "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", + }, + { + name: "-1", + description: + "Force output to be one entry per line. This is the default when output is not to a terminal", + }, + { + name: "-l", + description: + "List formulae and/or casks in long format. Has no effect when a formula or cask name is passed as an argument", + }, + { + name: "-r", + description: + "Reverse the order of the formulae and/or casks sort to list the oldest entries first. Has no effect when a formula or cask name is passed as an argument", + }, + { + name: "-t", + description: + "Sort formulae and/or casks by time modified, listing most recently modified first. Has no effect when a formula or cask name is passed as an argument", + }, + ], + args: { + isOptional: true, + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: "leaves", + description: + "List installed formulae that are not dependencies of another installed formula", + options: [ + { + name: ["-r", "--installed-on-request"], + description: "Show manually installed formula", + }, + { + name: ["-p", "--installed-as-dependency"], + description: "Show installed formula as dependencies", + }, + ], + }, + { + name: "doctor", + description: "Check your system for potential problems", + options: [ + ...commonOptions, + { + name: "--list-checks", + description: "List all audit methods", + }, + { + name: ["-D", "--audit-debug"], + description: "Enable debugging and profiling of audit methods", + }, + ], + }, + { + name: ["abv", "info"], + description: "Display brief statistics for your Homebrew installation", + args: { + isVariadic: true, + isOptional: true, + name: "formula", + description: "Formula or cask to summarize", + generators: [generateAllFormulae, generateAllCasks], + }, + options: [ + { + name: ["--cask", "--casks"], + description: "List only casks, or treat all named arguments as casks", + }, + { + name: "--analytics", + description: + "List global Homebrew analytics data or, if specified, installation and build error data for formula", + }, + { + name: "--days", + description: "How many days of analytics data to retrieve", + exclusiveOn: ["--analytics"], + args: { + name: "days", + description: "Number of days of data to retrieve", + suggestions: ["30", "90", "365"], + }, + }, + { + name: "--category", + description: "Which type of analytics data to retrieve", + exclusiveOn: ["--analytics"], + args: { + generators: { + custom: async (ctx) => { + // if anything provided after the subcommand does not begin with '-' + // then a formula has been provided and we should provide info on it + if ( + ctx.slice(2, ctx.length - 1).some((token) => token[0] !== "-") + ) { + return ["install", "install-on-request", "build-error"].map( + (sugg) => ({ + name: sugg, + }) + ); + } - // if no formulas are specified, then we should provide system info - return ["cask-install", "os-version"].map((sugg) => ({ - name: sugg, - })); - }, - }, - }, - }, - { - name: "--github", - description: "Open the GitHub source page for formula in a browser", - }, - { - name: "--json", - description: "Print a JSON representation", - }, - { - name: "--installed", - exclusiveOn: ["--json"], - description: "Print JSON of formulae that are currently installed", - }, - { - name: "--all", - exclusiveOn: ["--json"], - description: "Print JSON of all available formulae", - }, - { - name: ["-v", "--verbose"], - description: "Show more verbose analytics data for formulae", - }, - { - name: "--formula", - description: "Treat all named arguments as formulae", - }, - { - name: "--cash", - description: "Treat all named arguments as casks", - }, - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-q", "--quiet"], - description: "List only the names of outdated kegs", - }, - { - name: ["-h", "--help"], - description: "Get help with services command", - }, - ], - }, - { - name: "update", - description: "Fetch the newest version of Homebrew and all formulae", - options: [ - { - name: ["-f", "--force"], - description: "Always do a slower, full update check", - }, - { - name: ["-v", "--verbose"], - description: - "Print the directories checked and git operations performed", - }, - { - name: ["-d", "--debug"], - description: - "Display a trace of all shell commands as they are executed", - }, - { name: ["-h", "--help"], description: "Show help message" }, - { - name: "--merge", - description: - "Use git merge to apply updates (rather than git rebase)", - }, - { - name: "--preinstall", - description: - "Run on auto-updates (e.g. before brew install). Skips some slower steps", - }, - ], - }, - { - name: "outdated", - description: - "List installed casks and formulae that have an updated version available", - options: [ - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-q", "--quiet"], - description: "List only the names of outdated kegs", - }, - { - name: ["-v", "--verbose"], - description: "Include detailed version information", - }, - { - name: ["-h", "--help"], - description: "Show help message for the outdated command", - }, - { name: "--cask", description: "List only outdated casks" }, - { - name: "--fetch-HEAD", - description: - "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated", - }, - { name: "--formula", description: "List only outdated formulae" }, - { - name: "--greedy", - description: - "Print outdated casks with auto_updates or version :latest", - }, - { - name: "--greedy-latest", - description: - "Print outdated casks including those with version :latest", - }, - { - name: "--greedy-auto-updates", - description: - "Print outdated casks including those with auto_updates true", - }, - { name: "--json", description: "Print output in JSON format" }, - ], - }, - { - name: "pin", - description: "Pin formula, preventing them from being upgraded", - options: commonOptions, - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: "unpin", - description: "Unpin formula, allowing them to be upgraded", - options: commonOptions, - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: "upgrade", - description: - "Upgrade outdated casks and outdated, unpinned formulae using the same options they were originally installed with, plus any appended brew formula options", - options: [ - { - name: ["-d", "--debug"], - description: - "If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory", - }, - { - name: ["-f", "--force"], - description: - "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks, overwrite existing files (binaries and symlinks are excluded, unless originally from the same cask)", - }, - { - name: ["-v", "--verbose"], - description: "Print the verification and postinstall steps", - }, - { - name: ["-n", "--dry-run"], - description: - "Show what would be upgraded, but do not actually upgrade anything", - }, - { - name: ["-s", "--build-from-source"], - description: - "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", - }, - { - name: ["-i", "--interactive"], - description: "Download and patch formula, then open a shell", - }, - { name: ["-g", "--git"], description: "Create a Git repository" }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { name: ["-h", "--help"], description: "Show this message" }, - { - name: ["--formula", "--formulae"], - description: - "Treat all named arguments as formulae. If no named arguments are specified, upgrade only outdated formulae", - }, - { - name: "--env", - description: "Disabled other than for internal Homebrew use", - }, - { - name: "--ignore-dependencies", - description: - "An unsupported Homebrew development flag to skip installing any dependencies of any kind. If the dependencies are not already present, the formula will have issues. If you're not developing Homebrew, consider adjusting your PATH rather than using this flag", - }, - { - name: "--only-dependencies", - description: - "Install the dependencies with specified options but do not install the formula itself", - }, - { - name: "--cc", - description: - "Attempt to compile using the specified compiler, which should be the name of the compiler's executable", - args: { - name: "compiler", - suggestions: ["gcc-7", "llvm_clang", "clang"], - }, - }, - { - name: "--force-bottle", - description: - "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", - }, - { - name: "--include-test", - description: - "Install testing dependencies required to run brew test formula", - }, - { - name: "--HEAD", - description: - "If formula defines it, install the HEAD version, aka. main, trunk, unstable, master", - }, - { - name: "--fetch-HEAD", - description: - "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated. Otherwise, the repository's HEAD will only be checked for updates when a new stable or development version has been released", - }, - { - name: "--ignore-pinned", - description: - "Set a successful exit status even if pinned formulae are not upgraded", - }, - { - name: "--keep-tmp", - description: "Retain the temporary files created during installation", - }, - { - name: "--build-bottle", - description: - "Prepare the formula for eventual bottling during installation, skipping any post-install steps", - }, - { - name: "--bottle-arch", - description: - "Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on", - }, - { - name: "--display-times", - description: - "Print install times for each formula at the end of the run", - }, - { - name: ["--cask", "--casks"], - description: - "Treat all named arguments as casks. If no named arguments are specified, upgrade only outdated casks", - }, - { - name: "--binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - exclusiveOn: ["--no-binaries"], - }, - { - name: "--no-binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - exclusiveOn: ["--binaries"], - }, - { - name: "--require-sha", - description: "Require all casks to have a checksum", - }, - { - name: "--quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - exclusiveOn: ["--no-quarantine"], - }, - { - name: "--no-quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - exclusiveOn: ["--quarantine"], - }, - { - name: "--skip-cask-deps", - description: "Skip installing cask dependencies", - }, - { - name: "--greedy", - description: - "Also include casks with auto_updates true or version :latest", - exclusiveOn: ["--greedy-latest", "--greedy-auto-updates"], - }, - { - name: "--greedy-latest", - description: "Also include casks with version :latest", - }, - { - name: "--greedy-auto-updates", - description: "Also include casks with auto_updates true", - }, - { - name: "--appdir", - description: - "Target location for Applications (default: /Applications)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--colorpickerdir", - description: - "Target location for Color Pickers (default: ~/Library/ColorPickers)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--prefpanedir", - description: - "Target location for Preference Panes (default: ~/Library/PreferencePanes)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--qlplugindir", - description: - "Target location for QuickLook Plugins (default: ~/Library/QuickLook)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--mdimporterdir", - description: - "Target location for Spotlight Plugins (default: ~/Library/Spotlight)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--dictionarydir", - description: - "Target location for Dictionaries (default: ~/Library/Dictionaries)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--fontdir", - description: "Target location for Fonts (default: ~/Library/Fonts)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--servicedir", - description: - "Target location for Services (default: ~/Library/Services)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--input-methoddir", - description: - "Target location for Input Methods (default: ~/Library/Input Methods)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--internet-plugindir", - description: - "Target location for Internet Plugins (default: ~/Library/Internet Plug-Ins)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--audio-unit-plugindir", - description: - "Target location for Audio Unit Plugins (default: ~/Library/Audio/Plug-Ins/Components)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--vst-plugindir", - description: - "Target location for VST Plugins (default: ~/Library/Audio/Plug-Ins/VST)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--vst3-plugindir", - description: - "Target location for VST3 Plugins (default: ~/Library/Audio/Plug-Ins/VST3)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--screen-saverdir", - description: - "Target location for Screen Savers (default: ~/Library/Screen Savers)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--language", - description: - "Comma-separated list of language codes to prefer for cask installation. The first matching language is used, otherwise it reverts to the cask's default language. The default value is the language of your system", - }, - ], - args: { - isVariadic: true, - isOptional: true, - name: "outdated_formula|outdated_cask", - generators: outdatedformulaeGenerator, - }, - }, - { - name: "search", - description: - "Perform a substring search of cask tokens and formula names", - options: [ - ...commonOptions, - { - name: "--formula", - description: "Search online and locally for formulae", - }, - { - name: "--cask", - description: "Search online and locally for casks", - }, - { - name: "--desc", - description: - "Search for formulae with a description matching text and casks with a name matching text", - }, - { - name: "--pull-request", - description: "Search for GitHub pull requests containing text", - }, - { - name: "--open", - description: "Search for only open GitHub pull requests", - }, - { - name: "--closed", - description: "Search for only closed GitHub pull requests", - }, - { - name: ["--repology", "--macports"], - description: "Search for text in the given database", - }, - { - name: ["--fink", "--opensuse"], - description: "Search for text in the given database", - }, - { - name: ["--fedora", "--debian"], - description: "Search for text in the given database", - }, - { - name: "--ubuntu", - description: "Search for text in the given database", - }, - ], - }, - { - name: "config", - description: "Show Homebrew and system configuration info", - }, - { - name: "postinstall", - description: "Rerun the post install step for formula", - options: [ - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-v", "--verbose"], - description: "Make some output more verbose", - }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { name: ["-h", "--help"], description: "Show this message" }, - ], - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: "install", - description: "Install ", - options: [ - { - name: ["-f", "--force"], - description: - "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks", - }, - { - name: ["-v", "--verbose"], - description: "Print the verification and postinstall steps", - }, - { - name: ["-s", "--build-from-source"], - description: - "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", - }, - { - name: ["-i", "--interactive"], - description: "Download and patch formula", - }, - { name: ["-g", "--git"], description: "Create a Git repository" }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { name: ["-h", "--help"], description: "Show this message" }, - { - name: "--formula", - description: "Treat all named arguments as formulae", - }, - { - name: "--env", - description: "Disabled other than for internal Homebrew use", - }, - { - name: "--ignore-dependencies", - description: - "An unsupported Homebrew development flag to skip installing any dependencies of any kind. If the dependencies are not already present, the formula will have issues. If you're not developing Homebrew, consider adjusting your PATH rather than using this flag", - }, - { - name: "--only-dependencies", - description: - "Install the dependencies with specified options but do not install the formula itself", - }, - { - name: "--cc", - description: - "Attempt to compile using the specified compiler, which should be the name of the compiler's executable", - args: { - name: "compiler", - suggestions: ["gcc-7", "llvm_clang", "clang"], - }, - }, - { - name: "--force-bottle", - description: - "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", - }, - { - name: "--include-test", - description: - "Install testing dependencies required to run brew test formula", - }, - { - name: "--HEAD", - description: - "If formula defines it, install the HEAD version, aka. main, trunk, unstable, master", - }, - { - name: "--fetch-HEAD", - description: - "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated. Otherwise, the repository's HEAD will only be checked for updates when a new stable or development version has been released", - }, - { - name: "--keep-tmp", - description: "Retain the temporary files created during installation", - }, - { - name: "--build-bottle", - description: - "Prepare the formula for eventual bottling during installation, skipping any post-install steps", - }, - { - name: "--bottle-arch", - description: - "Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on", - }, - { - name: "--display-times", - description: - "Print install times for each formula at the end of the run", - }, - { - name: "--cask", - description: "--casks Treat all named arguments as casks", - }, - { - name: "--binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - }, - { - name: "--no-binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - }, - { - name: "--require-sha", - description: "Require all casks to have a checksum", - }, - { - name: "--quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - }, - { - name: "--no-quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - }, - { - name: "--skip-cask-deps", - description: "Skip installing cask dependencies", - }, - { - name: "--appdir", - description: - "Target location for Applications (default: /Applications)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--colorpickerdir", - description: - "Target location for Color Pickers (default: ~/Library/ColorPickers)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--prefpanedir", - description: - "Target location for Preference Panes (default: ~/Library/PreferencePanes)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--qlplugindir", - description: - "Target location for QuickLook Plugins (default: ~/Library/QuickLook)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--mdimporterdir", - description: - "Target location for Spotlight Plugins (default: ~/Library/Spotlight)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--dictionarydir", - description: - "Target location for Dictionaries (default: ~/Library/Dictionaries)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--fontdir", - description: "Target location for Fonts (default: ~/Library/Fonts)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--servicedir", - description: - "Target location for Services (default: ~/Library/Services)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--input-methoddir", - description: - "Target location for Input Methods (default: ~/Library/Input Methods)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--internet-plugindir", - description: - "Target location for Internet Plugins (default: ~/Library/Internet Plug-Ins)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--audio-unit-plugindir", - description: - "Target location for Audio Unit Plugins (default: ~/Library/Audio/Plug-Ins/Components)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--vst-plugindir", - description: - "Target location for VST Plugins (default: ~/Library/Audio/Plug-Ins/VST)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--vst3-plugindir", - description: - "Target location for VST3 Plugins (default: ~/Library/Audio/Plug-Ins/VST3)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--screen-saverdir", - description: - "Target location for Screen Savers (default: ~/Library/Screen Savers)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--language", - description: - "Comma-separated list of language codes to prefer for cask installation. The first matching language is used, otherwise it reverts to the cask's default language. The default value is the language of your system", - }, - ], - args: { - isVariadic: true, - name: "formula", - description: "Formula or cask to install", - generators: [generateAllFormulae, generateAllCasks], - }, - }, - { - name: "reinstall", - description: - "Uninstall and then reinstall a formula or cask using the same options it was originally installed with, plus any appended options specific to a formula", - options: [ - { - name: ["-d", "--debug"], - description: - "If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory", - }, - { - name: ["-f", "--force"], - description: - "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks", - }, - { - name: ["-v", "--verbose"], - description: "Print the verification and postinstall steps", - }, - { - name: ["-s", "--build-from-source"], - description: - "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", - }, - { - name: ["-i", "--interactive"], - description: "Download and patch formula", - }, - { name: ["-g", "--git"], description: "Create a Git repository" }, - { - name: "--formula", - description: "Treat all named arguments as formulae", - }, - { - name: "--force-bottle", - description: - "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", - }, - { - name: "--keep-tmp", - description: "Retain the temporary files created during installation", - }, - { - name: "--display-times", - description: - "Print install times for each formula at the end of the run", - }, - { - name: "--cask", - description: "--casks Treat all named arguments as casks", - }, - { - name: "--binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - exclusiveOn: ["--no-binaries"], - }, - { - name: "--no-binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - exclusiveOn: ["--binaries"], - }, - { - name: "--require-sha", - description: "Require all casks to have a checksum", - }, - { - name: "--quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - exclusiveOn: ["--no-quarantine"], - }, - { - name: "--no-quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - exclusiveOn: ["--quarantine"], - }, - { - name: "--skip-cask-deps", - description: "Skip installing cask dependencies", - }, - ], - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: ["uninstall", "remove", "rm"], - description: "Uninstall a formula or cask", - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - // NOTE: this is actually a command even if it has the double dash in the front - name: "--prefix", - description: "Prefix of ", - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - options: [ - { - name: "--unbrewed", - description: - "List files in Homebrew's prefix not installed by Homebrew", - }, - { - name: "--installed", - description: - "Outputs nothing and returns a failing status code if formula is not installed", - }, - ], - }, - { - name: "cask", - description: - "Homebrew Cask provides a friendly CLI workflow for the administration of macOS applications distributed as binaries", - subcommands: [ - { - name: "install", - description: "Installs the given cask", - args: { - name: "cask", - description: "Cask to install", - }, - }, - { - name: "uninstall", - description: "Uninstalls the given cask", - options: [ - ...commonOptions, - { - name: "--zap", - description: - "Remove all files associated with a cask. May remove files which are shared between applications", - }, - { - name: "--ignore-dependencies", - description: - "Don't fail uninstall, even if formula is a dependency of any installed formulae", - }, - { - name: "--formula", - description: "Treat all named arguments as formulae", - }, - { - name: "--cask", - description: "Treat all named arguments as casks", - }, - ], - args: { - isVariadic: true, + // if no formulas are specified, then we should provide system info + return ["cask-install", "os-version"].map((sugg) => ({ + name: sugg, + })); + }, + }, + }, + }, + { + name: "--github", + description: "Open the GitHub source page for formula in a browser", + }, + { + name: "--json", + description: "Print a JSON representation", + }, + { + name: "--installed", + exclusiveOn: ["--json"], + description: "Print JSON of formulae that are currently installed", + }, + { + name: "--all", + exclusiveOn: ["--json"], + description: "Print JSON of all available formulae", + }, + { + name: ["-v", "--verbose"], + description: "Show more verbose analytics data for formulae", + }, + { + name: "--formula", + description: "Treat all named arguments as formulae", + }, + { + name: "--cash", + description: "Treat all named arguments as casks", + }, + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-q", "--quiet"], + description: "List only the names of outdated kegs", + }, + { + name: ["-h", "--help"], + description: "Get help with services command", + }, + ], + }, + { + name: "update", + description: "Fetch the newest version of Homebrew and all formulae", + options: [ + { + name: ["-f", "--force"], + description: "Always do a slower, full update check", + }, + { + name: ["-v", "--verbose"], + description: + "Print the directories checked and git operations performed", + }, + { + name: ["-d", "--debug"], + description: + "Display a trace of all shell commands as they are executed", + }, + { name: ["-h", "--help"], description: "Show help message" }, + { + name: "--merge", + description: + "Use git merge to apply updates (rather than git rebase)", + }, + { + name: "--preinstall", + description: + "Run on auto-updates (e.g. before brew install). Skips some slower steps", + }, + ], + }, + { + name: "outdated", + description: + "List installed casks and formulae that have an updated version available", + options: [ + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-q", "--quiet"], + description: "List only the names of outdated kegs", + }, + { + name: ["-v", "--verbose"], + description: "Include detailed version information", + }, + { + name: ["-h", "--help"], + description: "Show help message for the outdated command", + }, + { name: "--cask", description: "List only outdated casks" }, + { + name: "--fetch-HEAD", + description: + "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated", + }, + { name: "--formula", description: "List only outdated formulae" }, + { + name: "--greedy", + description: + "Print outdated casks with auto_updates or version :latest", + }, + { + name: "--greedy-latest", + description: + "Print outdated casks including those with version :latest", + }, + { + name: "--greedy-auto-updates", + description: + "Print outdated casks including those with auto_updates true", + }, + { name: "--json", description: "Print output in JSON format" }, + ], + }, + { + name: "pin", + description: "Pin formula, preventing them from being upgraded", + options: commonOptions, + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: "unpin", + description: "Unpin formula, allowing them to be upgraded", + options: commonOptions, + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: "upgrade", + description: + "Upgrade outdated casks and outdated, unpinned formulae using the same options they were originally installed with, plus any appended brew formula options", + options: [ + { + name: ["-d", "--debug"], + description: + "If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory", + }, + { + name: ["-f", "--force"], + description: + "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks, overwrite existing files (binaries and symlinks are excluded, unless originally from the same cask)", + }, + { + name: ["-v", "--verbose"], + description: "Print the verification and postinstall steps", + }, + { + name: ["-n", "--dry-run"], + description: + "Show what would be upgraded, but do not actually upgrade anything", + }, + { + name: ["-s", "--build-from-source"], + description: + "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", + }, + { + name: ["-i", "--interactive"], + description: "Download and patch formula, then open a shell", + }, + { name: ["-g", "--git"], description: "Create a Git repository" }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { name: ["-h", "--help"], description: "Show this message" }, + { + name: ["--formula", "--formulae"], + description: + "Treat all named arguments as formulae. If no named arguments are specified, upgrade only outdated formulae", + }, + { + name: "--env", + description: "Disabled other than for internal Homebrew use", + }, + { + name: "--ignore-dependencies", + description: + "An unsupported Homebrew development flag to skip installing any dependencies of any kind. If the dependencies are not already present, the formula will have issues. If you're not developing Homebrew, consider adjusting your PATH rather than using this flag", + }, + { + name: "--only-dependencies", + description: + "Install the dependencies with specified options but do not install the formula itself", + }, + { + name: "--cc", + description: + "Attempt to compile using the specified compiler, which should be the name of the compiler's executable", + args: { + name: "compiler", + suggestions: ["gcc-7", "llvm_clang", "clang"], + }, + }, + { + name: "--force-bottle", + description: + "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", + }, + { + name: "--include-test", + description: + "Install testing dependencies required to run brew test formula", + }, + { + name: "--HEAD", + description: + "If formula defines it, install the HEAD version, aka. main, trunk, unstable, master", + }, + { + name: "--fetch-HEAD", + description: + "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated. Otherwise, the repository's HEAD will only be checked for updates when a new stable or development version has been released", + }, + { + name: "--ignore-pinned", + description: + "Set a successful exit status even if pinned formulae are not upgraded", + }, + { + name: "--keep-tmp", + description: "Retain the temporary files created during installation", + }, + { + name: "--build-bottle", + description: + "Prepare the formula for eventual bottling during installation, skipping any post-install steps", + }, + { + name: "--bottle-arch", + description: + "Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on", + }, + { + name: "--display-times", + description: + "Print install times for each formula at the end of the run", + }, + { + name: ["--cask", "--casks"], + description: + "Treat all named arguments as casks. If no named arguments are specified, upgrade only outdated casks", + }, + { + name: "--binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + exclusiveOn: ["--no-binaries"], + }, + { + name: "--no-binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + exclusiveOn: ["--binaries"], + }, + { + name: "--require-sha", + description: "Require all casks to have a checksum", + }, + { + name: "--quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + exclusiveOn: ["--no-quarantine"], + }, + { + name: "--no-quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + exclusiveOn: ["--quarantine"], + }, + { + name: "--skip-cask-deps", + description: "Skip installing cask dependencies", + }, + { + name: "--greedy", + description: + "Also include casks with auto_updates true or version :latest", + exclusiveOn: ["--greedy-latest", "--greedy-auto-updates"], + }, + { + name: "--greedy-latest", + description: "Also include casks with version :latest", + }, + { + name: "--greedy-auto-updates", + description: "Also include casks with auto_updates true", + }, + { + name: "--appdir", + description: + "Target location for Applications (default: /Applications)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--colorpickerdir", + description: + "Target location for Color Pickers (default: ~/Library/ColorPickers)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--prefpanedir", + description: + "Target location for Preference Panes (default: ~/Library/PreferencePanes)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--qlplugindir", + description: + "Target location for QuickLook Plugins (default: ~/Library/QuickLook)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--mdimporterdir", + description: + "Target location for Spotlight Plugins (default: ~/Library/Spotlight)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--dictionarydir", + description: + "Target location for Dictionaries (default: ~/Library/Dictionaries)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--fontdir", + description: "Target location for Fonts (default: ~/Library/Fonts)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--servicedir", + description: + "Target location for Services (default: ~/Library/Services)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--input-methoddir", + description: + "Target location for Input Methods (default: ~/Library/Input Methods)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--internet-plugindir", + description: + "Target location for Internet Plugins (default: ~/Library/Internet Plug-Ins)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--audio-unit-plugindir", + description: + "Target location for Audio Unit Plugins (default: ~/Library/Audio/Plug-Ins/Components)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--vst-plugindir", + description: + "Target location for VST Plugins (default: ~/Library/Audio/Plug-Ins/VST)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--vst3-plugindir", + description: + "Target location for VST3 Plugins (default: ~/Library/Audio/Plug-Ins/VST3)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--screen-saverdir", + description: + "Target location for Screen Savers (default: ~/Library/Screen Savers)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--language", + description: + "Comma-separated list of language codes to prefer for cask installation. The first matching language is used, otherwise it reverts to the cask's default language. The default value is the language of your system", + }, + ], + args: { + isVariadic: true, + isOptional: true, + name: "outdated_formula|outdated_cask", + generators: outdatedformulaeGenerator, + }, + }, + { + name: "search", + description: + "Perform a substring search of cask tokens and formula names", + options: [ + ...commonOptions, + { + name: "--formula", + description: "Search online and locally for formulae", + }, + { + name: "--cask", + description: "Search online and locally for casks", + }, + { + name: "--desc", + description: + "Search for formulae with a description matching text and casks with a name matching text", + }, + { + name: "--pull-request", + description: "Search for GitHub pull requests containing text", + }, + { + name: "--open", + description: "Search for only open GitHub pull requests", + }, + { + name: "--closed", + description: "Search for only closed GitHub pull requests", + }, + { + name: ["--repology", "--macports"], + description: "Search for text in the given database", + }, + { + name: ["--fink", "--opensuse"], + description: "Search for text in the given database", + }, + { + name: ["--fedora", "--debian"], + description: "Search for text in the given database", + }, + { + name: "--ubuntu", + description: "Search for text in the given database", + }, + ], + }, + { + name: "config", + description: "Show Homebrew and system configuration info", + }, + { + name: "postinstall", + description: "Rerun the post install step for formula", + options: [ + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-v", "--verbose"], + description: "Make some output more verbose", + }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { name: ["-h", "--help"], description: "Show this message" }, + ], + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: "install", + description: "Install ", + options: [ + { + name: ["-f", "--force"], + description: + "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks", + }, + { + name: ["-v", "--verbose"], + description: "Print the verification and postinstall steps", + }, + { + name: ["-s", "--build-from-source"], + description: + "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", + }, + { + name: ["-i", "--interactive"], + description: "Download and patch formula", + }, + { name: ["-g", "--git"], description: "Create a Git repository" }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { name: ["-h", "--help"], description: "Show this message" }, + { + name: "--formula", + description: "Treat all named arguments as formulae", + }, + { + name: "--env", + description: "Disabled other than for internal Homebrew use", + }, + { + name: "--ignore-dependencies", + description: + "An unsupported Homebrew development flag to skip installing any dependencies of any kind. If the dependencies are not already present, the formula will have issues. If you're not developing Homebrew, consider adjusting your PATH rather than using this flag", + }, + { + name: "--only-dependencies", + description: + "Install the dependencies with specified options but do not install the formula itself", + }, + { + name: "--cc", + description: + "Attempt to compile using the specified compiler, which should be the name of the compiler's executable", + args: { + name: "compiler", + suggestions: ["gcc-7", "llvm_clang", "clang"], + }, + }, + { + name: "--force-bottle", + description: + "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", + }, + { + name: "--include-test", + description: + "Install testing dependencies required to run brew test formula", + }, + { + name: "--HEAD", + description: + "If formula defines it, install the HEAD version, aka. main, trunk, unstable, master", + }, + { + name: "--fetch-HEAD", + description: + "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated. Otherwise, the repository's HEAD will only be checked for updates when a new stable or development version has been released", + }, + { + name: "--keep-tmp", + description: "Retain the temporary files created during installation", + }, + { + name: "--build-bottle", + description: + "Prepare the formula for eventual bottling during installation, skipping any post-install steps", + }, + { + name: "--bottle-arch", + description: + "Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on", + }, + { + name: "--display-times", + description: + "Print install times for each formula at the end of the run", + }, + { + name: "--cask", + description: "--casks Treat all named arguments as casks", + }, + { + name: "--binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + }, + { + name: "--no-binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + }, + { + name: "--require-sha", + description: "Require all casks to have a checksum", + }, + { + name: "--quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + }, + { + name: "--no-quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + }, + { + name: "--skip-cask-deps", + description: "Skip installing cask dependencies", + }, + { + name: "--appdir", + description: + "Target location for Applications (default: /Applications)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--colorpickerdir", + description: + "Target location for Color Pickers (default: ~/Library/ColorPickers)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--prefpanedir", + description: + "Target location for Preference Panes (default: ~/Library/PreferencePanes)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--qlplugindir", + description: + "Target location for QuickLook Plugins (default: ~/Library/QuickLook)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--mdimporterdir", + description: + "Target location for Spotlight Plugins (default: ~/Library/Spotlight)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--dictionarydir", + description: + "Target location for Dictionaries (default: ~/Library/Dictionaries)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--fontdir", + description: "Target location for Fonts (default: ~/Library/Fonts)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--servicedir", + description: + "Target location for Services (default: ~/Library/Services)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--input-methoddir", + description: + "Target location for Input Methods (default: ~/Library/Input Methods)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--internet-plugindir", + description: + "Target location for Internet Plugins (default: ~/Library/Internet Plug-Ins)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--audio-unit-plugindir", + description: + "Target location for Audio Unit Plugins (default: ~/Library/Audio/Plug-Ins/Components)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--vst-plugindir", + description: + "Target location for VST Plugins (default: ~/Library/Audio/Plug-Ins/VST)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--vst3-plugindir", + description: + "Target location for VST3 Plugins (default: ~/Library/Audio/Plug-Ins/VST3)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--screen-saverdir", + description: + "Target location for Screen Savers (default: ~/Library/Screen Savers)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--language", + description: + "Comma-separated list of language codes to prefer for cask installation. The first matching language is used, otherwise it reverts to the cask's default language. The default value is the language of your system", + }, + ], + args: { + isVariadic: true, + name: "formula", + description: "Formula or cask to install", + generators: [generateAllFormulae, generateAllCasks], + }, + }, + { + name: "reinstall", + description: + "Uninstall and then reinstall a formula or cask using the same options it was originally installed with, plus any appended options specific to a formula", + options: [ + { + name: ["-d", "--debug"], + description: + "If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory", + }, + { + name: ["-f", "--force"], + description: + "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks", + }, + { + name: ["-v", "--verbose"], + description: "Print the verification and postinstall steps", + }, + { + name: ["-s", "--build-from-source"], + description: + "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", + }, + { + name: ["-i", "--interactive"], + description: "Download and patch formula", + }, + { name: ["-g", "--git"], description: "Create a Git repository" }, + { + name: "--formula", + description: "Treat all named arguments as formulae", + }, + { + name: "--force-bottle", + description: + "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", + }, + { + name: "--keep-tmp", + description: "Retain the temporary files created during installation", + }, + { + name: "--display-times", + description: + "Print install times for each formula at the end of the run", + }, + { + name: "--cask", + description: "--casks Treat all named arguments as casks", + }, + { + name: "--binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + exclusiveOn: ["--no-binaries"], + }, + { + name: "--no-binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + exclusiveOn: ["--binaries"], + }, + { + name: "--require-sha", + description: "Require all casks to have a checksum", + }, + { + name: "--quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + exclusiveOn: ["--no-quarantine"], + }, + { + name: "--no-quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + exclusiveOn: ["--quarantine"], + }, + { + name: "--skip-cask-deps", + description: "Skip installing cask dependencies", + }, + ], + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: ["uninstall", "remove", "rm"], + description: "Uninstall a formula or cask", + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + // NOTE: this is actually a command even if it has the double dash in the front + name: "--prefix", + description: "Prefix of ", + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + options: [ + { + name: "--unbrewed", + description: + "List files in Homebrew's prefix not installed by Homebrew", + }, + { + name: "--installed", + description: + "Outputs nothing and returns a failing status code if formula is not installed", + }, + ], + }, + { + name: "cask", + description: + "Homebrew Cask provides a friendly CLI workflow for the administration of macOS applications distributed as binaries", + subcommands: [ + { + name: "install", + description: "Installs the given cask", + args: { + name: "cask", + description: "Cask to install", + }, + }, + { + name: "uninstall", + description: "Uninstalls the given cask", + options: [ + ...commonOptions, + { + name: "--zap", + description: + "Remove all files associated with a cask. May remove files which are shared between applications", + }, + { + name: "--ignore-dependencies", + description: + "Don't fail uninstall, even if formula is a dependency of any installed formulae", + }, + { + name: "--formula", + description: "Treat all named arguments as formulae", + }, + { + name: "--cask", + description: "Treat all named arguments as casks", + }, + ], + args: { + isVariadic: true, - generators: { - script: ["brew", "list", "-1", "--cask"], - postProcess: function (out) { - return out.split("\n").map((formula) => { - return { - name: formula, - icon: "🍺", - description: "Installed formula", - }; - }); - }, - }, - }, - }, - ], - }, - { - name: "cleanup", - description: - "Remove stale lock files and outdated downloads for all formulae and casks and remove old versions of installed formulae", - options: [ - ...commonOptions, - { - name: ["--prune", "--prune=all"], - description: "Remove all cache files older than specified days", - }, - { - name: ["-n", "--dry-run"], - description: - "Show what would be removed, but do not actually remove anything", - }, - { - name: "-s", - description: - "Scrub the cache, including downloads for even the latest versions", - }, - { - name: "--prune-prefix", - description: - "Only prune the symlinks and directories from the prefix and remove no other files", - }, - ], - args: { - isVariadic: true, - isOptional: true, - generators: servicesGenerator("Cleanup"), - }, - }, - { - name: "services", - description: - "Manage background services with macOS' launchctl(1) daemon manager", - options: [ - ...commonOptions, - { - name: "--file", - description: - "Use the plist file from this location to start or run the service", - }, - { - name: "--all", - description: "Run subcommand on all services", - }, - { - name: ["-v", "--verbose"], - description: "Make some output more verbose", - }, - { - name: ["-h", "--help"], - description: "Get help with services command", - }, - ], - subcommands: [ - { - name: "cleanup", - description: "Remove all unused services", - }, - { - name: "list", - description: "List all services", - }, - { - name: "run", - description: - "Run the service formula without registering to launch at login (or boot)", - options: [ - { - name: "--all", - description: "Start all services", - }, - ], - args: { - isVariadic: true, - generators: servicesGenerator("Run"), - }, - }, - { - name: "start", - description: - "Start the service formula immediately and register it to launch at login", - options: [ - { - name: "--all", - description: "Start all services", - }, - ], - args: { - isVariadic: true, - generators: servicesGenerator("Start"), - }, - }, - { - name: "stop", - description: - "Stop the service formula immediately and unregister it from launching at", - options: [ - { - name: "--all", - description: "Start all services", - }, - ], - args: { - isVariadic: true, - generators: servicesGenerator("Stop"), - }, - }, - { - name: "restart", - description: - "Stop (if necessary) and start the service formula immediately and register it to launch at login (or boot)", - options: [ - { - name: "--all", - description: "Start all services", - }, - ], - args: { - isVariadic: true, - generators: servicesGenerator("Restart"), - }, - }, - ], - }, - { - name: "analytics", - description: "Manages analytics preferences", - subcommands: [ - { - name: "on", - description: "Turns on analytics", - }, - { - name: "off", - description: "Turns off analytics", - }, - { - name: "regenerate-uuid", - description: "Regenerate the UUID used for analytics", - }, - ], - }, - { - name: "autoremove", - description: - "Uninstall formulae that were only installed as a dependency of another formula and are now no longer needed", - options: [ - { - name: ["-n", "--dry-run"], - description: - "List what would be uninstalled, but do not actually uninstall anything", - }, - ], - }, - { - name: "tap", - description: "Tap a formula repository", - options: [ - ...commonOptions, - { - name: "--full", - description: - "Convert a shallow clone to a full clone without untapping", - }, - { - name: "--shallow", - description: "Fetch tap as a shallow clone rather than a full clone", - }, - { - name: "--force-auto-update", - description: "Auto-update tap even if it is not hosted on GitHub", - }, - { - name: "--repair", - description: - "Migrate tapped formulae from symlink-based to directory-based structure", - }, - { - name: "--list-pinned", - description: "List all pinned taps", - }, - ], - args: { - name: "user/repo or URL", - }, - }, - { - name: "untap", - description: "Remove a tapped formula repository", - args: { - name: "repository", - generators: repositoriesGenerator(), - }, - options: [ - { - name: ["-f", "--force"], - description: - "Untap even if formulae or casks from this tap are currently installed", - }, - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { - name: ["-v", "--verbose"], - description: "Make some output more verbose", - }, - { - name: ["-h", "--help"], - description: "Show help message", - }, - ], - }, - { - name: "link", - description: - "Symlink all of formula's installed files into Homebrew's prefix", - args: { - isOptional: true, - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - options: [ - { - name: "--overwrite", - description: - "Delete files that already exist in the prefix while linking", - }, - { - name: ["-n", "--dry-run"], - description: - "List files which would be linked or deleted by brew link --overwrite without actually linking or deleting any files", - }, - { - name: ["-f", "--force"], - description: "Allow keg-only formulae to be linked", - }, - { - name: "--HEAD", - description: - "Link the HEAD version of the formula if it is installed", - }, - ], - }, - { - name: "unlink", - description: "Remove symlinks for formula from Homebrew's prefix", - args: { - isOptional: true, - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - options: [ - { - name: ["-n", "--dry-run"], - description: - "List files which would be unlinked without actually unlinking or deleting any files", - }, - ], - }, - { - name: "formulae", - description: "List all available formulae", - }, - { - name: "casks", - description: "List all available casks", - }, - { - name: "edit", - description: "", - args: { - isVariadic: true, - isOptional: true, - name: "formula", - description: "Formula or cask to install", - generators: [generateAllFormulae, generateAllCasks], - }, - options: [ - ...commonOptions, - { - name: ["--formula", "--formulae"], - description: "Treat all named arguments as formulae", - }, - { - name: ["--cask", "--casks"], - description: "Treat all named arguments as casks", - }, - ], - }, - { - name: ["home", "homepage"], - description: - "Open a formula, cask's homepage in a browser, or open Homebrew's own homepage if no argument is provided", - args: { - isVariadic: true, - isOptional: true, - name: "formula", - description: "Formula or cask to open homepage for", - generators: [generateAllFormulae, generateAllCasks], - }, - options: [ - ...commonOptions, - { - name: ["--formula", "--formulae"], - description: "Treat all named arguments as formulae", - }, - { - name: ["--cask", "--casks"], - description: "Treat all named arguments as casks", - }, - ], - }, - { - name: "alias", - description: "Manage custom user created brew aliases", - options: [ - { - name: "--edit", - description: "Edit aliases in a text editor", - }, - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { - name: ["-v", "--verbose"], - description: "Make some output more verbose", - }, - { - name: ["-h", "--help"], - description: "Show help message", - }, - ], - args: { - name: "alias", - generators: generateAliases, - description: "Display the alias command", - isOptional: true, - }, - }, - { - name: "developer", - description: "Display the current state of Homebrew's developer mode", - args: { - name: "state", - description: "Turn Homebrew's developer mode on or off respectively", - suggestions: ["on", "off"], - isOptional: true, - }, - }, - ], - options: [ - { - name: "--version", - description: "The current Homebrew version", - }, - ], - args: { - name: "alias", - generators: generateAliases, - description: "Custom user defined brew alias", - isOptional: true, - }, + generators: { + script: ["brew", "list", "-1", "--cask"], + postProcess: function (out) { + return out.split("\n").map((formula) => { + return { + name: formula, + icon: "🍺", + description: "Installed formula", + }; + }); + }, + }, + }, + }, + ], + }, + { + name: "cleanup", + description: + "Remove stale lock files and outdated downloads for all formulae and casks and remove old versions of installed formulae", + options: [ + ...commonOptions, + { + name: ["--prune", "--prune=all"], + description: "Remove all cache files older than specified days", + }, + { + name: ["-n", "--dry-run"], + description: + "Show what would be removed, but do not actually remove anything", + }, + { + name: "-s", + description: + "Scrub the cache, including downloads for even the latest versions", + }, + { + name: "--prune-prefix", + description: + "Only prune the symlinks and directories from the prefix and remove no other files", + }, + ], + args: { + isVariadic: true, + isOptional: true, + generators: servicesGenerator("Cleanup"), + }, + }, + { + name: "services", + description: + "Manage background services with macOS' launchctl(1) daemon manager", + options: [ + ...commonOptions, + { + name: "--file", + description: + "Use the plist file from this location to start or run the service", + }, + { + name: "--all", + description: "Run subcommand on all services", + }, + { + name: ["-v", "--verbose"], + description: "Make some output more verbose", + }, + { + name: ["-h", "--help"], + description: "Get help with services command", + }, + ], + subcommands: [ + { + name: "cleanup", + description: "Remove all unused services", + }, + { + name: "list", + description: "List all services", + }, + { + name: "run", + description: + "Run the service formula without registering to launch at login (or boot)", + options: [ + { + name: "--all", + description: "Start all services", + }, + ], + args: { + isVariadic: true, + generators: servicesGenerator("Run"), + }, + }, + { + name: "start", + description: + "Start the service formula immediately and register it to launch at login", + options: [ + { + name: "--all", + description: "Start all services", + }, + ], + args: { + isVariadic: true, + generators: servicesGenerator("Start"), + }, + }, + { + name: "stop", + description: + "Stop the service formula immediately and unregister it from launching at", + options: [ + { + name: "--all", + description: "Start all services", + }, + ], + args: { + isVariadic: true, + generators: servicesGenerator("Stop"), + }, + }, + { + name: "restart", + description: + "Stop (if necessary) and start the service formula immediately and register it to launch at login (or boot)", + options: [ + { + name: "--all", + description: "Start all services", + }, + ], + args: { + isVariadic: true, + generators: servicesGenerator("Restart"), + }, + }, + ], + }, + { + name: "analytics", + description: "Manages analytics preferences", + subcommands: [ + { + name: "on", + description: "Turns on analytics", + }, + { + name: "off", + description: "Turns off analytics", + }, + { + name: "regenerate-uuid", + description: "Regenerate the UUID used for analytics", + }, + ], + }, + { + name: "autoremove", + description: + "Uninstall formulae that were only installed as a dependency of another formula and are now no longer needed", + options: [ + { + name: ["-n", "--dry-run"], + description: + "List what would be uninstalled, but do not actually uninstall anything", + }, + ], + }, + { + name: "tap", + description: "Tap a formula repository", + options: [ + ...commonOptions, + { + name: "--full", + description: + "Convert a shallow clone to a full clone without untapping", + }, + { + name: "--shallow", + description: "Fetch tap as a shallow clone rather than a full clone", + }, + { + name: "--force-auto-update", + description: "Auto-update tap even if it is not hosted on GitHub", + }, + { + name: "--repair", + description: + "Migrate tapped formulae from symlink-based to directory-based structure", + }, + { + name: "--list-pinned", + description: "List all pinned taps", + }, + ], + args: { + name: "user/repo or URL", + }, + }, + { + name: "untap", + description: "Remove a tapped formula repository", + args: { + name: "repository", + generators: repositoriesGenerator(), + }, + options: [ + { + name: ["-f", "--force"], + description: + "Untap even if formulae or casks from this tap are currently installed", + }, + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { + name: ["-v", "--verbose"], + description: "Make some output more verbose", + }, + { + name: ["-h", "--help"], + description: "Show help message", + }, + ], + }, + { + name: "link", + description: + "Symlink all of formula's installed files into Homebrew's prefix", + args: { + isOptional: true, + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + options: [ + { + name: "--overwrite", + description: + "Delete files that already exist in the prefix while linking", + }, + { + name: ["-n", "--dry-run"], + description: + "List files which would be linked or deleted by brew link --overwrite without actually linking or deleting any files", + }, + { + name: ["-f", "--force"], + description: "Allow keg-only formulae to be linked", + }, + { + name: "--HEAD", + description: + "Link the HEAD version of the formula if it is installed", + }, + ], + }, + { + name: "unlink", + description: "Remove symlinks for formula from Homebrew's prefix", + args: { + isOptional: true, + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + options: [ + { + name: ["-n", "--dry-run"], + description: + "List files which would be unlinked without actually unlinking or deleting any files", + }, + ], + }, + { + name: "formulae", + description: "List all available formulae", + }, + { + name: "casks", + description: "List all available casks", + }, + { + name: "edit", + description: "", + args: { + isVariadic: true, + isOptional: true, + name: "formula", + description: "Formula or cask to install", + generators: [generateAllFormulae, generateAllCasks], + }, + options: [ + ...commonOptions, + { + name: ["--formula", "--formulae"], + description: "Treat all named arguments as formulae", + }, + { + name: ["--cask", "--casks"], + description: "Treat all named arguments as casks", + }, + ], + }, + { + name: ["home", "homepage"], + description: + "Open a formula, cask's homepage in a browser, or open Homebrew's own homepage if no argument is provided", + args: { + isVariadic: true, + isOptional: true, + name: "formula", + description: "Formula or cask to open homepage for", + generators: [generateAllFormulae, generateAllCasks], + }, + options: [ + ...commonOptions, + { + name: ["--formula", "--formulae"], + description: "Treat all named arguments as formulae", + }, + { + name: ["--cask", "--casks"], + description: "Treat all named arguments as casks", + }, + ], + }, + { + name: "alias", + description: "Manage custom user created brew aliases", + options: [ + { + name: "--edit", + description: "Edit aliases in a text editor", + }, + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { + name: ["-v", "--verbose"], + description: "Make some output more verbose", + }, + { + name: ["-h", "--help"], + description: "Show help message", + }, + ], + args: { + name: "alias", + generators: generateAliases, + description: "Display the alias command", + isOptional: true, + }, + }, + { + name: "developer", + description: "Display the current state of Homebrew's developer mode", + args: { + name: "state", + description: "Turn Homebrew's developer mode on or off respectively", + suggestions: ["on", "off"], + isOptional: true, + }, + }, + ], + options: [ + { + name: "--version", + description: "The current Homebrew version", + }, + ], + args: { + name: "alias", + generators: generateAliases, + description: "Custom user defined brew alias", + isOptional: true, + }, }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/cat.ts b/extensions/terminal-suggest/src/completions/upstream/cat.ts index 4e06dacd8b4..b67e19b9c59 100644 --- a/extensions/terminal-suggest/src/completions/upstream/cat.ts +++ b/extensions/terminal-suggest/src/completions/upstream/cat.ts @@ -1,50 +1,50 @@ const completionSpec: Fig.Spec = { - name: "cat", - description: "Concatenate and print files", - args: { - isVariadic: true, - template: "filepaths", - }, - options: [ - { - name: "-b", - description: "Number the non-blank output lines, starting at 1", - }, + name: "cat", + description: "Concatenate and print files", + args: { + isVariadic: true, + template: "filepaths", + }, + options: [ + { + name: "-b", + description: "Number the non-blank output lines, starting at 1", + }, - { - name: "-e", - description: - "Display non-printing characters (see the -v option), and display a dollar sign (‘$’) at the end of each line", - }, + { + name: "-e", + description: + "Display non-printing characters (see the -v option), and display a dollar sign (‘$’) at the end of each line", + }, - { - name: "-l", - description: - "Set an exclusive advisory lock on the standard output file descriptor. This lock is set using fcntl(2) with the F_SETLKW command. If the output file is already locked, cat will block until the lock is acquired", - }, + { + name: "-l", + description: + "Set an exclusive advisory lock on the standard output file descriptor. This lock is set using fcntl(2) with the F_SETLKW command. If the output file is already locked, cat will block until the lock is acquired", + }, - { name: "-n", description: "Number the output lines, starting at 1" }, + { name: "-n", description: "Number the output lines, starting at 1" }, - { - name: "-s", - description: - "Squeeze multiple adjacent empty lines, causing the output to be single spaced", - }, + { + name: "-s", + description: + "Squeeze multiple adjacent empty lines, causing the output to be single spaced", + }, - { - name: "-t", - description: - "Display non-printing characters (see the -v option), and display tab characters as ‘^I’", - }, + { + name: "-t", + description: + "Display non-printing characters (see the -v option), and display tab characters as ‘^I’", + }, - { name: "-u", description: "Disable output buffering" }, + { name: "-u", description: "Disable output buffering" }, - { - name: "-v", - description: - "Display non-printing characters so they are visible. Control characters print as ‘^X’ for control-X; the delete character (octal 0177) prints as ‘^?’. Non-ASCII characters (with the high bit set) are printed as ‘M-’ (for meta) followed by the character for the low 7 bits", - }, - ], + { + name: "-v", + description: + "Display non-printing characters so they are visible. Control characters print as ‘^X’ for control-X; the delete character (octal 0177) prints as ‘^?’. Non-ASCII characters (with the high bit set) are printed as ‘M-’ (for meta) followed by the character for the low 7 bits", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/chmod.ts b/extensions/terminal-suggest/src/completions/upstream/chmod.ts index 93bc7264e8b..c3b72a01a2f 100644 --- a/extensions/terminal-suggest/src/completions/upstream/chmod.ts +++ b/extensions/terminal-suggest/src/completions/upstream/chmod.ts @@ -1,88 +1,88 @@ const completionSpec: Fig.Spec = { - name: "chmod", - description: "Change file modes or Access Control Lists", - args: [ - { - name: "mode", - suggestions: [ - // Some of the most common chmod's (non-exhaustive) - { - name: "u+x", - type: "arg", - description: "Give execute permission for the user", - icon: "🔐", - }, - { - name: "a+rx", - type: "arg", - description: "Adds read and execute permissions for all classes", - icon: "🔐", - }, - { - name: "744", - type: "arg", - description: - "Sets read, write, and execute permissions for user, and sets read permission for Group and Others", - icon: "🔐", - }, - { - name: "664", - type: "arg", - description: - "Sets read and write permissions for user and Group, and provides read to Others", - icon: "🔐", - }, - { - name: "777", - type: "arg", - description: "⚠️ allows all actions for all users", - icon: "🔐", - }, - ], - }, - { - // Modifying - template: "filepaths", - }, - ], + name: "chmod", + description: "Change file modes or Access Control Lists", + args: [ + { + name: "mode", + suggestions: [ + // Some of the most common chmod's (non-exhaustive) + { + name: "u+x", + type: "arg", + description: "Give execute permission for the user", + icon: "🔐", + }, + { + name: "a+rx", + type: "arg", + description: "Adds read and execute permissions for all classes", + icon: "🔐", + }, + { + name: "744", + type: "arg", + description: + "Sets read, write, and execute permissions for user, and sets read permission for Group and Others", + icon: "🔐", + }, + { + name: "664", + type: "arg", + description: + "Sets read and write permissions for user and Group, and provides read to Others", + icon: "🔐", + }, + { + name: "777", + type: "arg", + description: "⚠️ allows all actions for all users", + icon: "🔐", + }, + ], + }, + { + // Modifying + template: "filepaths", + }, + ], - options: [ - { - name: "-f", - description: - "Do not display a diagnostic message if chmod could not modify the mode for file, nor modify the exit status to reflect such failures", - }, - { - name: "-H", - description: - "If the -R option is specified, symbolic links on the command line are followed and hence unaffected by the command. (Symbolic links encountered during tree traversal are not followed.)", - }, - { - name: "-h", - description: - "If the file is a symbolic link, change the mode of the link itself rather than the file that the link points to", - }, - { - name: "-L", - description: - "If the -R option is specified, all symbolic links are followed", - }, - { - name: "-P", - description: - "If the -R option is specified, no symbolic links are followed. This is the default", - }, - { - name: "-R", - description: - "Change the modes of the file hierarchies rooted in the files, instead of just the files themselves. Beware of unintentionally matching the ``..'' hard link to the parent directory when using wildcards like ``.*''", - }, - { - name: "-v", - description: - "Cause chmod to be verbose, showing filenames as the mode is modified. If the -v flag is specified more than once, the old and new modes of the file will also be printed, in both octal and symbolic notation", - }, - ], + options: [ + { + name: "-f", + description: + "Do not display a diagnostic message if chmod could not modify the mode for file, nor modify the exit status to reflect such failures", + }, + { + name: "-H", + description: + "If the -R option is specified, symbolic links on the command line are followed and hence unaffected by the command. (Symbolic links encountered during tree traversal are not followed.)", + }, + { + name: "-h", + description: + "If the file is a symbolic link, change the mode of the link itself rather than the file that the link points to", + }, + { + name: "-L", + description: + "If the -R option is specified, all symbolic links are followed", + }, + { + name: "-P", + description: + "If the -R option is specified, no symbolic links are followed. This is the default", + }, + { + name: "-R", + description: + "Change the modes of the file hierarchies rooted in the files, instead of just the files themselves. Beware of unintentionally matching the ``..'' hard link to the parent directory when using wildcards like ``.*''", + }, + { + name: "-v", + description: + "Cause chmod to be verbose, showing filenames as the mode is modified. If the -v flag is specified more than once, the old and new modes of the file will also be printed, in both octal and symbolic notation", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/chown.ts b/extensions/terminal-suggest/src/completions/upstream/chown.ts index c271be0bd40..3ee4f0f0142 100644 --- a/extensions/terminal-suggest/src/completions/upstream/chown.ts +++ b/extensions/terminal-suggest/src/completions/upstream/chown.ts @@ -1,117 +1,117 @@ export const existingUsersandGroups: Fig.Generator = { - custom: async function (tokens, executeShellCommand) { - const colonAdded = tokens.find((token) => token.includes(":")); - const nFlagUsed = tokens.find((token) => /^-.*n.*/.test(token)); + custom: async function (tokens, executeShellCommand) { + const colonAdded = tokens.find((token) => token.includes(":")); + const nFlagUsed = tokens.find((token) => /^-.*n.*/.test(token)); - let shell: string; - // Using `:` as a trigger, check to see if a colon is added - // in the current command. If it is, get the system groups - // else retrieve the list of system users - if (colonAdded) { - const { stdout } = await executeShellCommand({ - command: "bash", - args: [ - "-c", - "dscl . -list /Groups PrimaryGroupID | tr -s ' '| sort -r", - ], - }); - shell = stdout; - } else { - const { stdout } = await executeShellCommand({ - command: "bash", - args: ["-c", "dscl . -list /Users UniqueID | tr -s ' '| sort -r"], - }); - shell = stdout; - } + let shell: string; + // Using `:` as a trigger, check to see if a colon is added + // in the current command. If it is, get the system groups + // else retrieve the list of system users + if (colonAdded) { + const { stdout } = await executeShellCommand({ + command: "bash", + args: [ + "-c", + "dscl . -list /Groups PrimaryGroupID | tr -s ' '| sort -r", + ], + }); + shell = stdout; + } else { + const { stdout } = await executeShellCommand({ + command: "bash", + args: ["-c", "dscl . -list /Users UniqueID | tr -s ' '| sort -r"], + }); + shell = stdout; + } - return ( - shell - .split("\n") - // The shell command retrieves a table - // with rows that look like `user uid` - // so each row is split again to get the - // user/group and uid/gid - .map((line) => line.split(" ")) - .map((value) => { - return { - // If the user has entered the option n - // suggest the uid/gid instead of user/group - name: nFlagUsed ? value[1] : value[0], - description: colonAdded - ? `Group - ${nFlagUsed ? value[0] : `gid: ${value[1]}`}` - : `User - ${nFlagUsed ? value[0] : `uid: ${value[1]}`}`, - icon: colonAdded ? "👥" : "👤", - priority: 90, - }; - }) - ); - }, - trigger: ":", - getQueryTerm: ":", + return ( + shell + .split("\n") + // The shell command retrieves a table + // with rows that look like `user uid` + // so each row is split again to get the + // user/group and uid/gid + .map((line) => line.split(" ")) + .map((value) => { + return { + // If the user has entered the option n + // suggest the uid/gid instead of user/group + name: nFlagUsed ? value[1] : value[0], + description: colonAdded + ? `Group - ${nFlagUsed ? value[0] : `gid: ${value[1]}`}` + : `User - ${nFlagUsed ? value[0] : `uid: ${value[1]}`}`, + icon: colonAdded ? "👥" : "👤", + priority: 90, + }; + }) + ); + }, + trigger: ":", + getQueryTerm: ":", }; const completionSpec: Fig.Spec = { - name: "chown", - description: - "Change the user and/or group ownership of a given file, directory, or symbolic link", - args: [ - { - name: "owner[:group] or :group", - generators: existingUsersandGroups, - }, - { - name: "file/directory", - isVariadic: true, - template: ["filepaths", "folders"], - }, - ], - options: [ - { - name: "-f", - description: - "Don't report any failure to change file owner or group, nor modify the exit status to reflect such failures", - }, - { - name: "-h", - description: - "If the file is a symbolic link, change the user ID and/or the group ID of the link itself", - }, - { - name: "-n", - description: - "Interpret user ID and group ID as numeric, avoiding name lookups", - }, - { - name: "-v", - description: - "Cause chown to be verbose, showing files as the owner is modified", - }, - { - name: "-R", - description: - "Change the user ID and/or the group ID for the file hierarchies rooted in the files instead of just the files themselves", - }, - { - name: "-H", - description: - "If the -R option is specified, symbolic links on the command line are followed", - exclusiveOn: ["-L", "-P"], - dependsOn: ["-R"], - }, - { - name: "-L", - description: - "If the -R option is specified, all symbolic links are followed", - exclusiveOn: ["-H", "-P"], - dependsOn: ["-R"], - }, - { - name: "-P", - description: - "If the -R option is specified, no symbolic links are followed", - exclusiveOn: ["-H", "-L"], - dependsOn: ["-R"], - }, - ], + name: "chown", + description: + "Change the user and/or group ownership of a given file, directory, or symbolic link", + args: [ + { + name: "owner[:group] or :group", + generators: existingUsersandGroups, + }, + { + name: "file/directory", + isVariadic: true, + template: ["filepaths", "folders"], + }, + ], + options: [ + { + name: "-f", + description: + "Don't report any failure to change file owner or group, nor modify the exit status to reflect such failures", + }, + { + name: "-h", + description: + "If the file is a symbolic link, change the user ID and/or the group ID of the link itself", + }, + { + name: "-n", + description: + "Interpret user ID and group ID as numeric, avoiding name lookups", + }, + { + name: "-v", + description: + "Cause chown to be verbose, showing files as the owner is modified", + }, + { + name: "-R", + description: + "Change the user ID and/or the group ID for the file hierarchies rooted in the files instead of just the files themselves", + }, + { + name: "-H", + description: + "If the -R option is specified, symbolic links on the command line are followed", + exclusiveOn: ["-L", "-P"], + dependsOn: ["-R"], + }, + { + name: "-L", + description: + "If the -R option is specified, all symbolic links are followed", + exclusiveOn: ["-H", "-P"], + dependsOn: ["-R"], + }, + { + name: "-P", + description: + "If the -R option is specified, no symbolic links are followed", + exclusiveOn: ["-H", "-L"], + dependsOn: ["-R"], + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/cp.ts b/extensions/terminal-suggest/src/completions/upstream/cp.ts index 65d3d065cfd..91a05f48e65 100644 --- a/extensions/terminal-suggest/src/completions/upstream/cp.ts +++ b/extensions/terminal-suggest/src/completions/upstream/cp.ts @@ -1,79 +1,79 @@ const completionSpec: Fig.Spec = { - name: "cp", - description: "Copy files and directories", - args: [ - { - name: "source", - template: ["filepaths", "folders"], - isVariadic: true, - }, - { - name: "target", - template: ["filepaths", "folders"], - }, - ], - options: [ - { - name: "-a", - description: - "Preserves structure and attributes of files but not directory structure", - }, - { - name: "-f", - description: - "If the destination file cannot be opened, remove it and create a new file, without prompting for confirmation", - exclusiveOn: ["-n"], - }, - { - name: "-H", - description: - "If the -R option is specified, symbolic links on the command line are followed", - exclusiveOn: ["-L", "-P"], - dependsOn: ["-R"], - }, - { - name: "-i", - description: - "Cause cp to write a prompt to the standard error output before copying a file that would overwrite an existing file", - exclusiveOn: ["-n"], - }, - { - name: "-L", - description: - "If the -R option is specified, all symbolic links are followed", - exclusiveOn: ["-H", "-P"], - dependsOn: ["-R"], - }, - { - name: "-n", - description: "Do not overwrite an existing file", - exclusiveOn: ["-f", "-i"], - }, - { - name: "-P", - description: - "If the -R option is specified, no symbolic links are followed", - exclusiveOn: ["-H", "-L"], - dependsOn: ["-R"], - }, - { - name: "-R", - description: - "If source designates a directory, cp copies the directory and the entire subtree connected at that point. If source ends in a /, the contents of the directory are copied rather than the directory itself", - }, - { - name: "-v", - description: "Cause cp to be verbose, showing files as they are copied", - }, - { - name: "-X", - description: "Do not copy Extended Attributes (EAs) or resource forks", - }, - { - name: "-c", - description: "Copy files using clonefile", - }, - ], + name: "cp", + description: "Copy files and directories", + args: [ + { + name: "source", + template: ["filepaths", "folders"], + isVariadic: true, + }, + { + name: "target", + template: ["filepaths", "folders"], + }, + ], + options: [ + { + name: "-a", + description: + "Preserves structure and attributes of files but not directory structure", + }, + { + name: "-f", + description: + "If the destination file cannot be opened, remove it and create a new file, without prompting for confirmation", + exclusiveOn: ["-n"], + }, + { + name: "-H", + description: + "If the -R option is specified, symbolic links on the command line are followed", + exclusiveOn: ["-L", "-P"], + dependsOn: ["-R"], + }, + { + name: "-i", + description: + "Cause cp to write a prompt to the standard error output before copying a file that would overwrite an existing file", + exclusiveOn: ["-n"], + }, + { + name: "-L", + description: + "If the -R option is specified, all symbolic links are followed", + exclusiveOn: ["-H", "-P"], + dependsOn: ["-R"], + }, + { + name: "-n", + description: "Do not overwrite an existing file", + exclusiveOn: ["-f", "-i"], + }, + { + name: "-P", + description: + "If the -R option is specified, no symbolic links are followed", + exclusiveOn: ["-H", "-L"], + dependsOn: ["-R"], + }, + { + name: "-R", + description: + "If source designates a directory, cp copies the directory and the entire subtree connected at that point. If source ends in a /, the contents of the directory are copied rather than the directory itself", + }, + { + name: "-v", + description: "Cause cp to be verbose, showing files as they are copied", + }, + { + name: "-X", + description: "Do not copy Extended Attributes (EAs) or resource forks", + }, + { + name: "-c", + description: "Copy files using clonefile", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/curl.ts b/extensions/terminal-suggest/src/completions/upstream/curl.ts index d47732b6447..eb6cfd319c4 100644 --- a/extensions/terminal-suggest/src/completions/upstream/curl.ts +++ b/extensions/terminal-suggest/src/completions/upstream/curl.ts @@ -1,914 +1,914 @@ const completionSpec: Fig.Spec = { - name: "curl", - description: "Transfer a URL", - args: { name: "URL", template: "history" }, - options: [ - { - name: ["-a", "--append"], - description: "Append to target file when uploading", - }, - { - name: ["-E", "--cert"], - description: "Client certificate file and password", - args: { - name: "certificate[:password]", - generators: { - getQueryTerm: ":", - }, - }, - }, - { - name: ["-K", "--config"], - description: "Read config from a file", - args: { name: "file", template: "filepaths" }, - }, - { - name: ["-C", "--continue-at"], - description: "Resumed transfer offset", - args: { name: "offset" }, - }, - { - name: ["-b", "--cookie"], - description: "Send cookies from string/file", - args: { name: "data or filename", template: "filepaths" }, - }, - { - name: ["-c", "--cookie-jar"], - description: "Write cookies to after operation", - args: { name: "filename", template: "filepaths" }, - }, - { - name: ["-d", "--data"], - description: "HTTP POST data", - insertValue: "-d '{cursor}'", - args: { name: "data" }, - isRepeatable: true, - }, - { name: ["-q", "--disable"], description: "Disable .curlrc" }, - { - name: ["-D", "--dump-header"], - description: "Write the received headers to ", - args: { name: "filename", template: "filepaths" }, - }, - { - name: ["-f", "--fail"], - description: "Fail silently (no output at all) on HTTP errors", - }, - { - name: ["-F", "--form"], - description: "Specify multipart MIME data", - args: { name: "content" }, - isRepeatable: true, - }, - { - name: ["-P", "--ftp-port"], - description: "Use PORT instead of PASV", - args: { name: "address" }, - }, - { - name: ["-G", "--get"], - description: "Put the post data in the URL and use GET", - }, - { - name: ["-g", "--globoff"], - description: "Disable URL sequences and ranges using {} and []", - }, - { name: ["-I", "--head"], description: "Show document info only" }, - { - name: ["-H", "--header"], - description: "Pass custom header(s) to server", - args: { - name: "header/file", - suggestions: [ - { name: "Content-Type: application/json" }, - { name: "Content-Type: application/x-www-form-urlencoded" }, - ], - }, - }, - { name: ["-h", "--help"], description: "This help text" }, - { name: ["-0", "--http1.0"], description: "Use HTTP 1.0" }, - { - name: ["-i", "--include"], - description: "Include protocol response headers in the output", - }, - { - name: ["-k", "--insecure"], - description: "Allow insecure server connections when using SSL", - }, - { name: ["-4", "--ipv4"], description: "Resolve names to IPv4 addresses" }, - { name: ["-6", "--ipv6"], description: "Resolve names to IPv6 addresses" }, - { - name: ["-j", "--junk-session-cookies"], - description: "Ignore session cookies read from file", - }, - { name: ["-l", "--list-only"], description: "List only mode" }, - { name: ["-L", "--location"], description: "Follow redirects" }, - { name: ["-M", "--manual"], description: "Display the full manual" }, - { - name: ["-m", "--max-time"], - description: "Maximum time allowed for the transfer", - args: { name: "seconds" }, - }, - { - name: ["-n", "--netrc"], - description: "Must read .netrc for user name and password", - }, - { - name: ["-:", "--next"], - description: "Make next URL use its separate set of options", - }, - { - name: ["-N", "--no-buffer"], - description: "Disable buffering of the output stream", - }, - { - name: ["-o", "--output"], - description: "Write to file instead of stdout", - args: { name: "file", template: "filepaths" }, - }, - { - name: ["-#", "--progress-bar"], - description: "Display transfer progress as a bar", - }, - { - name: ["-x", "--proxy"], - description: "[protocol://]host[:port] Use this proxy", - }, - { - name: ["-U", "--proxy-user"], - description: "Proxy user and password", - args: { name: "user:password" }, - }, - { - name: ["-p", "--proxytunnel"], - description: "Operate through an HTTP proxy tunnel (using CONNECT)", - }, - { - name: ["-Q", "--quote"], - description: "Send command(s) to server before transfer", - }, - { - name: ["-r", "--range"], - description: "Retrieve only the bytes within RANGE", - args: { name: "range" }, - }, - { - name: ["-e", "--referer"], - description: "Referrer URL", - args: { name: "URL" }, - }, - { - name: ["-J", "--remote-header-name"], - description: "Use the header-provided filename", - }, - { - name: ["-O", "--remote-name"], - description: "Write output to a file named as the remote file", - }, - { - name: ["-R", "--remote-time"], - description: "Set the remote file's time on the local output", - }, - { - name: ["-X", "--request"], - description: "Specify request command to use", - args: { - name: "command", - suggestions: [ - { name: "GET" }, - { name: "HEAD" }, - { name: "POST" }, - { name: "PUT" }, - { name: "DELETE" }, - { name: "CONNECT" }, - { name: "OPTIONS" }, - { name: "TRACE" }, - { name: "PATCH" }, - ], - }, - }, - { - name: ["-S", "--show-error"], - description: "Show error even when -s is used", - }, - { name: ["-s", "--silent"], description: "Silent mode" }, - { - name: ["-Y", "--speed-limit"], - description: "Stop transfers slower than this", - args: { name: "speed" }, - }, - { - name: ["-y", "--speed-time"], - description: "Trigger 'speed-limit' abort after this time", - args: { name: "seconds" }, - }, - { name: ["-2", "--sslv2"], description: "Use SSLv2" }, - { name: ["-3", "--sslv3"], description: "Use SSLv3" }, - { - name: ["-t", "--telnet-option"], - description: "Set telnet option", - args: { name: "val" }, - }, - { - name: ["-z", "--time-cond"], - description: "Transfer based on a time condition", - args: { name: "time" }, - }, - { name: ["-1", "--tlsv1"], description: "Use TLSv1.0 or greater" }, - { - name: ["-T", "--upload-file"], - description: "Transfer local FILE to destination", - args: { name: "file", template: "filepaths" }, - }, - { name: ["-B", "--use-ascii"], description: "Use ASCII/text transfer" }, - { - name: ["-u", "--user"], - description: "Server user and password", - args: { name: "user:password" }, - }, - { - name: ["-A", "--user-agent"], - description: "Send User-Agent to server", - args: { name: "name" }, - }, - { - name: ["-v", "--verbose"], - description: "Make the operation more talkative", - }, - { name: ["-V", "--version"], description: "Show version number and quit" }, - { - name: ["-w", "--write-out"], - description: "Use output FORMAT after completion", - args: { name: "format" }, - }, - { - name: "--abstract-unix-socket", - description: "Connect via abstract Unix domain socket", - args: { name: "path" }, - }, - { - name: "--alt-svc", - description: "Name> Enable alt-svc with this cache file", - args: { name: "file", template: "filepaths" }, - }, - { name: "--anyauth", description: "Pick any authentication method" }, - { name: "--basic", description: "Use HTTP Basic Authentication" }, - { - name: "--cacert", - description: "CA certificate to verify peer against", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--capath", - description: "CA directory to verify peer against", - args: { name: "dir", template: "folders" }, - }, - { - name: "--cert-status", - description: "Verify the status of the server certificate", - }, - { - name: "--cert-type", - description: "Certificate file type", - args: { - name: "type", - suggestions: [{ name: "DER" }, { name: "PEM" }, { name: "ENG" }], - }, - }, - { - name: "--ciphers", - description: "Of ciphers> SSL ciphers to use", - args: { name: "list" }, - }, - { name: "--compressed", description: "Request compressed response" }, - { name: "--compressed-ssh", description: "Enable SSH compression" }, - { - name: "--connect-timeout", - description: "Maximum time allowed for connection", - args: { name: "seconds" }, - }, - { - name: "--connect-to", - description: "Connect to host", - args: { name: "HOST1:PORT1:HOST2:PORT2" }, - }, - { - name: "--create-dirs", - description: "Create necessary local directory hierarchy", - }, - { name: "--crlf", description: "Convert LF to CRLF in upload" }, - { - name: "--crlfile", - description: "Get a CRL list in PEM format from the given file", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--data-ascii", - description: "HTTP POST ASCII data", - args: { name: "data" }, - }, - { - name: "--data-binary", - description: "HTTP POST binary data", - args: { name: "data" }, - }, - { - name: "--data-raw", - description: "HTTP POST data, '@' allowed", - args: { name: "data" }, - }, - { - name: "--data-urlencode", - description: "HTTP POST data url encoded", - args: { name: "data" }, - }, - { - name: "--delegation", - description: "GSS-API delegation permission", - args: { name: "LEVEL" }, - }, - { name: "--digest", description: "Use HTTP Digest Authentication" }, - { name: "--disable-eprt", description: "Inhibit using EPRT or LPRT" }, - { name: "--disable-epsv", description: "Inhibit using EPSV" }, - { - name: "--disallow-username-in-url", - description: "Disallow username in url", - }, - { - name: "--dns-interface", - description: "Interface to use for DNS requests", - args: { name: "interface" }, - }, - { - name: "--dns-ipv4-addr", - description: "IPv4 address to use for DNS requests", - args: { name: "address" }, - }, - { - name: "--dns-ipv6-addr", - description: "IPv6 address to use for DNS requests", - args: { name: "address" }, - }, - { - name: "--dns-servers", - description: "DNS server addrs to use", - args: { name: "addresses" }, - }, - { - name: "--doh-url", - description: "Resolve host names over DOH", - args: { name: "URL" }, - }, - { - name: "--egd-file", - description: "EGD socket path for random data", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--engine", - description: "Crypto engine to use", - args: { name: "name" }, - }, - { - name: "--etag-compare", - description: - "Make a conditional HTTP request for the ETag read from the given file", - args: { name: "file" }, - }, - { - name: "--etag-save", - description: "Save an HTTP ETag to the specified file", - args: { name: "file" }, - }, - { - name: "--expect100-timeout", - description: "How long to wait for 100-continue", - args: { name: "seconds" }, - }, - { - name: "--fail-early", - description: "Fail on first transfer error, do not continue", - }, - { - name: "--fail-with-body", - description: - "On HTTP errors, return an error and also output any HTML response", - }, - { name: "--false-start", description: "Enable TLS False Start" }, - { - name: "--form-string", - description: "Specify multipart MIME data", - args: { name: "string" }, - }, - { - name: "--ftp-account", - description: "Account data string", - args: { name: "data" }, - }, - { - name: "--ftp-alternative-to-user", - description: "String to replace USER [name]", - args: { name: "command" }, - }, - { - name: "--ftp-create-dirs", - description: "Create the remote dirs if not present", - }, - { - name: "--ftp-method", - description: "Control CWD usage", - args: { name: "method" }, - }, - { name: "--ftp-pasv", description: "Use PASV/EPSV instead of PORT" }, - { name: "--ftp-pret", description: "Send PRET before PASV" }, - { name: "--ftp-skip-pasv-ip", description: "Skip the IP address for PASV" }, - { name: "--ftp-ssl-ccc", description: "Send CCC after authenticating" }, - { - name: "--ftp-ssl-ccc-mode", - description: "Set CCC mode", - args: { - name: "mode", - suggestions: [{ name: "active" }, { name: "passive" }], - }, - }, - { - name: "--ftp-ssl-control", - description: "Require SSL/TLS for FTP login, clear for transfer", - }, - { - name: "--happy-eyeballs-timeout-ms", - description: - "How long to wait in milliseconds for IPv6 before trying IPv4", - args: { name: "milliseconds" }, - }, - { - name: "--haproxy-protocol", - description: "Send HAProxy PROXY protocol v1 header", - }, - { - name: "--hostpubmd5", - description: "Acceptable MD5 hash of the host public key", - args: { name: "md5" }, - }, - { name: "--http0.9", description: "Allow HTTP 0.9 responses" }, - { name: "--http1.1", description: "Use HTTP 1.1" }, - { name: "--http2", description: "Use HTTP 2" }, - { - name: "--http2-prior-knowledge", - description: "Use HTTP 2 without HTTP/1.1 Upgrade", - }, - { - name: "--ignore-content-length", - description: "Ignore the size of the remote resource", - }, - { - name: "--interface", - description: "Use network INTERFACE (or address)", - args: { name: "name" }, - }, - { - name: "--keepalive-time", - description: "Interval time for keepalive probes", - args: { name: "seconds" }, - }, - { - name: "--key", - description: "Private key file name", - args: { name: "key" }, - }, - { - name: "--key-type", - description: "Private key file type", - args: { - name: "type", - suggestions: [{ name: "DER" }, { name: "PEM" }, { name: "ENG" }], - }, - }, - { - name: "--krb", - description: "Enable Kerberos with security ", - args: { name: "level" }, - }, - { - name: "--libcurl", - description: "Dump libcurl equivalent code of this command line", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--limit-rate", - description: "Limit transfer speed to RATE", - args: { name: "speed" }, - }, - { - name: "--local-port", - description: "Force use of RANGE for local port numbers", - args: { name: "num/range" }, - }, - { - name: "--location-trusted", - description: "Like --location, and send auth to other hosts", - }, - { - name: "--login-options", - description: "Server login options", - args: { name: "options" }, - }, - { - name: "--mail-auth", - description: "Originator address of the original email", - args: { name: "address" }, - }, - { - name: "--mail-from", - description: "Mail from this address", - args: { name: "address" }, - }, - { - name: "--mail-rcpt", - description: "Mail to this address", - args: { name: "address" }, - }, - { - name: "--max-filesize", - description: "Maximum file size to download", - args: { name: "bytes" }, - }, - { - name: "--max-redirs", - description: "Maximum number of redirects allowed", - args: { name: "num" }, - }, - { - name: "--metalink", - description: "Process given URLs as metalink XML file", - }, - { - name: "--negotiate", - description: "Use HTTP Negotiate (SPNEGO) authentication", - }, - { - name: "--netrc-file", - description: "Specify FILE for netrc", - args: { name: "filename", template: "filepaths" }, - }, - { name: "--netrc-optional", description: "Use either .netrc or URL" }, - { name: "--no-alpn", description: "Disable the ALPN TLS extension" }, - { - name: "--no-keepalive", - description: "Disable TCP keepalive on the connection", - }, - { name: "--no-npn", description: "Disable the NPN TLS extension" }, - { name: "--no-sessionid", description: "Disable SSL session-ID reusing" }, - { - name: "--noproxy", - description: "List of hosts which do not use proxy", - args: { name: "no-proxy-list" }, - }, - { name: "--ntlm", description: "Use HTTP NTLM authentication" }, - { - name: "--ntlm-wb", - description: "Use HTTP NTLM authentication with winbind", - }, - { - name: "--oauth2-bearer", - description: "OAuth 2 Bearer Token", - args: { name: "token" }, - }, - { - name: "--pass", - description: "Pass phrase for the private key", - args: { name: "phrase" }, - }, - { - name: "--path-as-is", - description: "Do not squash .. sequences in URL path", - }, - { - name: "--pinnedpubkey", - description: "FILE/HASHES Public key to verify peer against", - args: { name: "hashes" }, - }, - { - name: "--post301", - description: "Do not switch to GET after following a 301", - }, - { - name: "--post302", - description: "Do not switch to GET after following a 302", - }, - { - name: "--post303", - description: "Do not switch to GET after following a 303", - }, - { - name: "--preproxy", - description: "[protocol://]host[:port] Use this proxy first", - }, - { - name: "--proto", - description: "Enable/disable PROTOCOLS", - args: { name: "protocols" }, - }, - { - name: "--proto-default", - description: "Use PROTOCOL for any URL missing a scheme", - args: { name: "protocol" }, - }, - { - name: "--proto-redir", - description: "Enable/disable PROTOCOLS on redirect", - args: { name: "protocols" }, - }, - { - name: "--proxy-anyauth", - description: "Pick any proxy authentication method", - }, - { - name: "--proxy-basic", - description: "Use Basic authentication on the proxy", - }, - { - name: "--proxy-cacert", - description: "CA certificate to verify peer against for proxy", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--proxy-capath", - description: "CA directory to verify peer against for proxy", - args: { name: "dir", template: "folders" }, - }, - { - name: "--proxy-cert", - description: "Set client certificate for proxy", - args: { name: "cert[:passwd]" }, - }, - { - name: "--proxy-cert-type", - description: "Client certificate type for HTTPS proxy", - args: { name: "type" }, - }, - { - name: "--proxy-ciphers", - description: "SSL ciphers to use for proxy", - args: { name: "list" }, - }, - { - name: "--proxy-crlfile", - description: "Set a CRL list for proxy", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--proxy-digest", - description: "Use Digest authentication on the proxy", - }, - { - name: "--proxy-header", - description: "Pass custom header(s) to proxy", - args: { - name: "header/file", - suggestions: [ - { name: "Content-Type: application/json" }, - { name: "Content-Type: application/x-www-form-urlencoded" }, - ], - }, - }, - { - name: "--proxy-insecure", - description: "Do HTTPS proxy connections without verifying the proxy", - }, - { - name: "--proxy-key", - description: "Private key for HTTPS proxy", - args: { name: "key" }, - }, - { - name: "--proxy-key-type", - description: "Private key file type for proxy", - args: { name: "type" }, - }, - { - name: "--proxy-negotiate", - description: "Use HTTP Negotiate (SPNEGO) authentication on the proxy", - }, - { - name: "--proxy-ntlm", - description: "Use NTLM authentication on the proxy", - }, - { - name: "--proxy-pass", - description: "Pass phrase for the private key for HTTPS proxy", - args: { name: "phrase" }, - }, - { - name: "--proxy-pinnedpubkey", - description: "FILE/HASHES public key to verify proxy with", - args: { name: "hashes" }, - }, - { - name: "--proxy-service-name", - description: "SPNEGO proxy service name", - args: { name: "name" }, - }, - { - name: "--proxy-ssl-allow-beast", - description: "Allow security flaw for interop for HTTPS proxy", - }, - { - name: "--proxy-tls13-ciphers", - description: "List> TLS 1.3 proxy cipher suites", - args: { name: "ciphersuite" }, - }, - { - name: "--proxy-tlsauthtype", - description: "TLS authentication type for HTTPS proxy", - args: { name: "type" }, - }, - { - name: "--proxy-tlspassword", - description: "TLS password for HTTPS proxy", - args: { name: "string" }, - }, - { - name: "--proxy-tlsuser", - description: "TLS username for HTTPS proxy", - args: { name: "name" }, - }, - { name: "--proxy-tlsv1", description: "Use TLSv1 for HTTPS proxy" }, - { - name: "--proxy1.0", - description: "Use HTTP/1.0 proxy on given port", - args: { name: "host[:port]" }, - }, - { - name: "--pubkey", - description: "SSH Public key file name", - args: { name: "key", template: "filepaths" }, - }, - { - name: "--random-file", - description: "File for reading random data from", - args: { name: "file", template: "filepaths" }, - }, - { name: "--raw", description: 'Do HTTP "raw"; no transfer decoding' }, - { - name: "--remote-name-all", - description: "Use the remote file name for all URLs", - }, - { - name: "--request-target", - description: "Specify the target for this request", - }, - { - name: "--resolve", - description: "Resolve the host+port to this address", - args: { name: "host:port:address[,address]..." }, - }, - { - name: "--retry", - description: "Retry request if transient problems occur", - args: { name: "num" }, - }, - { - name: "--retry-connrefused", - description: "Retry on connection refused (use with --retry)", - }, - { - name: "--retry-delay", - description: "Wait time between retries", - args: { name: "seconds" }, - }, - { - name: "--retry-max-time", - description: "Retry only within this period", - args: { name: "seconds" }, - }, - { - name: "--sasl-ir", - description: "Enable initial response in SASL authentication", - }, - { - name: "--service-name", - description: "SPNEGO service name", - args: { name: "name" }, - }, - { - name: "--socks4", - description: "SOCKS4 proxy on given host + port", - args: { name: "host[:port]" }, - }, - { - name: "--socks4a", - description: "SOCKS4a proxy on given host + port", - args: { name: "host[:port]" }, - }, - { - name: "--socks5", - description: "SOCKS5 proxy on given host + port", - args: { name: "host[:port]" }, - }, - { - name: "--socks5-basic", - description: "Enable username/password auth for SOCKS5 proxies", - }, - { - name: "--socks5-gssapi", - description: "Enable GSS-API auth for SOCKS5 proxies", - }, - { - name: "--socks5-gssapi-nec", - description: "Compatibility with NEC SOCKS5 server", - }, - { - name: "--socks5-gssapi-service", - description: "SOCKS5 proxy service name for GSS-API", - args: { name: "name" }, - }, - { - name: "--socks5-hostname", - description: "SOCKS5 proxy, pass host name to proxy", - args: { name: "host[:port]" }, - }, - { name: "--ssl", description: "Try SSL/TLS" }, - { - name: "--ssl-auto-client-cert", - description: "Obtain and use a client certificate automatically", - }, - { - name: "--ssl-allow-beast", - description: "Allow security flaw to improve interop", - }, - { - name: "--ssl-no-revoke", - description: "Disable cert revocation checks (Schannel)", - }, - { name: "--ssl-reqd", description: "Require SSL/TLS" }, - { name: "--stderr", description: "Where to redirect stderr" }, - { - name: "--styled-output", - description: "Enable styled output for HTTP headers", - }, - { - name: "--suppress-connect-headers", - description: "Suppress proxy CONNECT response headers", - }, - { name: "--tcp-fastopen", description: "Use TCP Fast Open" }, - { name: "--tcp-nodelay", description: "Use the TCP_NODELAY option" }, - { - name: "--tftp-blksize", - description: "Set TFTP BLKSIZE option", - args: { name: "value" }, - }, - { name: "--tftp-no-options", description: "Do not send any TFTP options" }, - { - name: "--tls-max", - description: "Set maximum allowed TLS version", - args: { name: "VERSION" }, - }, - { - name: "--tls13-ciphers", - description: "Of TLS 1.3 ciphersuites> TLS 1.3 cipher suites to use", - args: { name: "list" }, - }, - { - name: "--tlsauthtype", - description: "TLS authentication type", - args: { name: "type" }, - }, - { name: "--tlspassword", description: "TLS password" }, - { name: "--tlsuser", description: "TLS user name", args: { name: "name" } }, - { name: "--tlsv1.0", description: "Use TLSv1.0 or greater" }, - { name: "--tlsv1.1", description: "Use TLSv1.1 or greater" }, - { name: "--tlsv1.2", description: "Use TLSv1.2 or greater" }, - { name: "--tlsv1.3", description: "Use TLSv1.3 or greater" }, - { - name: "--tr-encoding", - description: "Request compressed transfer encoding", - }, - { - name: "--trace", - description: "Write a debug trace to FILE", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--trace-ascii", - description: "Like --trace, but without hex output", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--trace-time", - description: "Add time stamps to trace/verbose output", - }, - { - name: "--unix-socket", - description: "Connect through this Unix domain socket", - args: { name: "path" }, - }, - { name: "--url", description: "URL to work with", args: { name: "url" } }, - { - name: "--xattr", - description: "Store metadata in extended file attributes", - }, - ], + name: "curl", + description: "Transfer a URL", + args: { name: "URL", template: "history" }, + options: [ + { + name: ["-a", "--append"], + description: "Append to target file when uploading", + }, + { + name: ["-E", "--cert"], + description: "Client certificate file and password", + args: { + name: "certificate[:password]", + generators: { + getQueryTerm: ":", + }, + }, + }, + { + name: ["-K", "--config"], + description: "Read config from a file", + args: { name: "file", template: "filepaths" }, + }, + { + name: ["-C", "--continue-at"], + description: "Resumed transfer offset", + args: { name: "offset" }, + }, + { + name: ["-b", "--cookie"], + description: "Send cookies from string/file", + args: { name: "data or filename", template: "filepaths" }, + }, + { + name: ["-c", "--cookie-jar"], + description: "Write cookies to after operation", + args: { name: "filename", template: "filepaths" }, + }, + { + name: ["-d", "--data"], + description: "HTTP POST data", + insertValue: "-d '{cursor}'", + args: { name: "data" }, + isRepeatable: true, + }, + { name: ["-q", "--disable"], description: "Disable .curlrc" }, + { + name: ["-D", "--dump-header"], + description: "Write the received headers to ", + args: { name: "filename", template: "filepaths" }, + }, + { + name: ["-f", "--fail"], + description: "Fail silently (no output at all) on HTTP errors", + }, + { + name: ["-F", "--form"], + description: "Specify multipart MIME data", + args: { name: "content" }, + isRepeatable: true, + }, + { + name: ["-P", "--ftp-port"], + description: "Use PORT instead of PASV", + args: { name: "address" }, + }, + { + name: ["-G", "--get"], + description: "Put the post data in the URL and use GET", + }, + { + name: ["-g", "--globoff"], + description: "Disable URL sequences and ranges using {} and []", + }, + { name: ["-I", "--head"], description: "Show document info only" }, + { + name: ["-H", "--header"], + description: "Pass custom header(s) to server", + args: { + name: "header/file", + suggestions: [ + { name: "Content-Type: application/json" }, + { name: "Content-Type: application/x-www-form-urlencoded" }, + ], + }, + }, + { name: ["-h", "--help"], description: "This help text" }, + { name: ["-0", "--http1.0"], description: "Use HTTP 1.0" }, + { + name: ["-i", "--include"], + description: "Include protocol response headers in the output", + }, + { + name: ["-k", "--insecure"], + description: "Allow insecure server connections when using SSL", + }, + { name: ["-4", "--ipv4"], description: "Resolve names to IPv4 addresses" }, + { name: ["-6", "--ipv6"], description: "Resolve names to IPv6 addresses" }, + { + name: ["-j", "--junk-session-cookies"], + description: "Ignore session cookies read from file", + }, + { name: ["-l", "--list-only"], description: "List only mode" }, + { name: ["-L", "--location"], description: "Follow redirects" }, + { name: ["-M", "--manual"], description: "Display the full manual" }, + { + name: ["-m", "--max-time"], + description: "Maximum time allowed for the transfer", + args: { name: "seconds" }, + }, + { + name: ["-n", "--netrc"], + description: "Must read .netrc for user name and password", + }, + { + name: ["-:", "--next"], + description: "Make next URL use its separate set of options", + }, + { + name: ["-N", "--no-buffer"], + description: "Disable buffering of the output stream", + }, + { + name: ["-o", "--output"], + description: "Write to file instead of stdout", + args: { name: "file", template: "filepaths" }, + }, + { + name: ["-#", "--progress-bar"], + description: "Display transfer progress as a bar", + }, + { + name: ["-x", "--proxy"], + description: "[protocol://]host[:port] Use this proxy", + }, + { + name: ["-U", "--proxy-user"], + description: "Proxy user and password", + args: { name: "user:password" }, + }, + { + name: ["-p", "--proxytunnel"], + description: "Operate through an HTTP proxy tunnel (using CONNECT)", + }, + { + name: ["-Q", "--quote"], + description: "Send command(s) to server before transfer", + }, + { + name: ["-r", "--range"], + description: "Retrieve only the bytes within RANGE", + args: { name: "range" }, + }, + { + name: ["-e", "--referer"], + description: "Referrer URL", + args: { name: "URL" }, + }, + { + name: ["-J", "--remote-header-name"], + description: "Use the header-provided filename", + }, + { + name: ["-O", "--remote-name"], + description: "Write output to a file named as the remote file", + }, + { + name: ["-R", "--remote-time"], + description: "Set the remote file's time on the local output", + }, + { + name: ["-X", "--request"], + description: "Specify request command to use", + args: { + name: "command", + suggestions: [ + { name: "GET" }, + { name: "HEAD" }, + { name: "POST" }, + { name: "PUT" }, + { name: "DELETE" }, + { name: "CONNECT" }, + { name: "OPTIONS" }, + { name: "TRACE" }, + { name: "PATCH" }, + ], + }, + }, + { + name: ["-S", "--show-error"], + description: "Show error even when -s is used", + }, + { name: ["-s", "--silent"], description: "Silent mode" }, + { + name: ["-Y", "--speed-limit"], + description: "Stop transfers slower than this", + args: { name: "speed" }, + }, + { + name: ["-y", "--speed-time"], + description: "Trigger 'speed-limit' abort after this time", + args: { name: "seconds" }, + }, + { name: ["-2", "--sslv2"], description: "Use SSLv2" }, + { name: ["-3", "--sslv3"], description: "Use SSLv3" }, + { + name: ["-t", "--telnet-option"], + description: "Set telnet option", + args: { name: "val" }, + }, + { + name: ["-z", "--time-cond"], + description: "Transfer based on a time condition", + args: { name: "time" }, + }, + { name: ["-1", "--tlsv1"], description: "Use TLSv1.0 or greater" }, + { + name: ["-T", "--upload-file"], + description: "Transfer local FILE to destination", + args: { name: "file", template: "filepaths" }, + }, + { name: ["-B", "--use-ascii"], description: "Use ASCII/text transfer" }, + { + name: ["-u", "--user"], + description: "Server user and password", + args: { name: "user:password" }, + }, + { + name: ["-A", "--user-agent"], + description: "Send User-Agent to server", + args: { name: "name" }, + }, + { + name: ["-v", "--verbose"], + description: "Make the operation more talkative", + }, + { name: ["-V", "--version"], description: "Show version number and quit" }, + { + name: ["-w", "--write-out"], + description: "Use output FORMAT after completion", + args: { name: "format" }, + }, + { + name: "--abstract-unix-socket", + description: "Connect via abstract Unix domain socket", + args: { name: "path" }, + }, + { + name: "--alt-svc", + description: "Name> Enable alt-svc with this cache file", + args: { name: "file", template: "filepaths" }, + }, + { name: "--anyauth", description: "Pick any authentication method" }, + { name: "--basic", description: "Use HTTP Basic Authentication" }, + { + name: "--cacert", + description: "CA certificate to verify peer against", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--capath", + description: "CA directory to verify peer against", + args: { name: "dir", template: "folders" }, + }, + { + name: "--cert-status", + description: "Verify the status of the server certificate", + }, + { + name: "--cert-type", + description: "Certificate file type", + args: { + name: "type", + suggestions: [{ name: "DER" }, { name: "PEM" }, { name: "ENG" }], + }, + }, + { + name: "--ciphers", + description: "Of ciphers> SSL ciphers to use", + args: { name: "list" }, + }, + { name: "--compressed", description: "Request compressed response" }, + { name: "--compressed-ssh", description: "Enable SSH compression" }, + { + name: "--connect-timeout", + description: "Maximum time allowed for connection", + args: { name: "seconds" }, + }, + { + name: "--connect-to", + description: "Connect to host", + args: { name: "HOST1:PORT1:HOST2:PORT2" }, + }, + { + name: "--create-dirs", + description: "Create necessary local directory hierarchy", + }, + { name: "--crlf", description: "Convert LF to CRLF in upload" }, + { + name: "--crlfile", + description: "Get a CRL list in PEM format from the given file", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--data-ascii", + description: "HTTP POST ASCII data", + args: { name: "data" }, + }, + { + name: "--data-binary", + description: "HTTP POST binary data", + args: { name: "data" }, + }, + { + name: "--data-raw", + description: "HTTP POST data, '@' allowed", + args: { name: "data" }, + }, + { + name: "--data-urlencode", + description: "HTTP POST data url encoded", + args: { name: "data" }, + }, + { + name: "--delegation", + description: "GSS-API delegation permission", + args: { name: "LEVEL" }, + }, + { name: "--digest", description: "Use HTTP Digest Authentication" }, + { name: "--disable-eprt", description: "Inhibit using EPRT or LPRT" }, + { name: "--disable-epsv", description: "Inhibit using EPSV" }, + { + name: "--disallow-username-in-url", + description: "Disallow username in url", + }, + { + name: "--dns-interface", + description: "Interface to use for DNS requests", + args: { name: "interface" }, + }, + { + name: "--dns-ipv4-addr", + description: "IPv4 address to use for DNS requests", + args: { name: "address" }, + }, + { + name: "--dns-ipv6-addr", + description: "IPv6 address to use for DNS requests", + args: { name: "address" }, + }, + { + name: "--dns-servers", + description: "DNS server addrs to use", + args: { name: "addresses" }, + }, + { + name: "--doh-url", + description: "Resolve host names over DOH", + args: { name: "URL" }, + }, + { + name: "--egd-file", + description: "EGD socket path for random data", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--engine", + description: "Crypto engine to use", + args: { name: "name" }, + }, + { + name: "--etag-compare", + description: + "Make a conditional HTTP request for the ETag read from the given file", + args: { name: "file" }, + }, + { + name: "--etag-save", + description: "Save an HTTP ETag to the specified file", + args: { name: "file" }, + }, + { + name: "--expect100-timeout", + description: "How long to wait for 100-continue", + args: { name: "seconds" }, + }, + { + name: "--fail-early", + description: "Fail on first transfer error, do not continue", + }, + { + name: "--fail-with-body", + description: + "On HTTP errors, return an error and also output any HTML response", + }, + { name: "--false-start", description: "Enable TLS False Start" }, + { + name: "--form-string", + description: "Specify multipart MIME data", + args: { name: "string" }, + }, + { + name: "--ftp-account", + description: "Account data string", + args: { name: "data" }, + }, + { + name: "--ftp-alternative-to-user", + description: "String to replace USER [name]", + args: { name: "command" }, + }, + { + name: "--ftp-create-dirs", + description: "Create the remote dirs if not present", + }, + { + name: "--ftp-method", + description: "Control CWD usage", + args: { name: "method" }, + }, + { name: "--ftp-pasv", description: "Use PASV/EPSV instead of PORT" }, + { name: "--ftp-pret", description: "Send PRET before PASV" }, + { name: "--ftp-skip-pasv-ip", description: "Skip the IP address for PASV" }, + { name: "--ftp-ssl-ccc", description: "Send CCC after authenticating" }, + { + name: "--ftp-ssl-ccc-mode", + description: "Set CCC mode", + args: { + name: "mode", + suggestions: [{ name: "active" }, { name: "passive" }], + }, + }, + { + name: "--ftp-ssl-control", + description: "Require SSL/TLS for FTP login, clear for transfer", + }, + { + name: "--happy-eyeballs-timeout-ms", + description: + "How long to wait in milliseconds for IPv6 before trying IPv4", + args: { name: "milliseconds" }, + }, + { + name: "--haproxy-protocol", + description: "Send HAProxy PROXY protocol v1 header", + }, + { + name: "--hostpubmd5", + description: "Acceptable MD5 hash of the host public key", + args: { name: "md5" }, + }, + { name: "--http0.9", description: "Allow HTTP 0.9 responses" }, + { name: "--http1.1", description: "Use HTTP 1.1" }, + { name: "--http2", description: "Use HTTP 2" }, + { + name: "--http2-prior-knowledge", + description: "Use HTTP 2 without HTTP/1.1 Upgrade", + }, + { + name: "--ignore-content-length", + description: "Ignore the size of the remote resource", + }, + { + name: "--interface", + description: "Use network INTERFACE (or address)", + args: { name: "name" }, + }, + { + name: "--keepalive-time", + description: "Interval time for keepalive probes", + args: { name: "seconds" }, + }, + { + name: "--key", + description: "Private key file name", + args: { name: "key" }, + }, + { + name: "--key-type", + description: "Private key file type", + args: { + name: "type", + suggestions: [{ name: "DER" }, { name: "PEM" }, { name: "ENG" }], + }, + }, + { + name: "--krb", + description: "Enable Kerberos with security ", + args: { name: "level" }, + }, + { + name: "--libcurl", + description: "Dump libcurl equivalent code of this command line", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--limit-rate", + description: "Limit transfer speed to RATE", + args: { name: "speed" }, + }, + { + name: "--local-port", + description: "Force use of RANGE for local port numbers", + args: { name: "num/range" }, + }, + { + name: "--location-trusted", + description: "Like --location, and send auth to other hosts", + }, + { + name: "--login-options", + description: "Server login options", + args: { name: "options" }, + }, + { + name: "--mail-auth", + description: "Originator address of the original email", + args: { name: "address" }, + }, + { + name: "--mail-from", + description: "Mail from this address", + args: { name: "address" }, + }, + { + name: "--mail-rcpt", + description: "Mail to this address", + args: { name: "address" }, + }, + { + name: "--max-filesize", + description: "Maximum file size to download", + args: { name: "bytes" }, + }, + { + name: "--max-redirs", + description: "Maximum number of redirects allowed", + args: { name: "num" }, + }, + { + name: "--metalink", + description: "Process given URLs as metalink XML file", + }, + { + name: "--negotiate", + description: "Use HTTP Negotiate (SPNEGO) authentication", + }, + { + name: "--netrc-file", + description: "Specify FILE for netrc", + args: { name: "filename", template: "filepaths" }, + }, + { name: "--netrc-optional", description: "Use either .netrc or URL" }, + { name: "--no-alpn", description: "Disable the ALPN TLS extension" }, + { + name: "--no-keepalive", + description: "Disable TCP keepalive on the connection", + }, + { name: "--no-npn", description: "Disable the NPN TLS extension" }, + { name: "--no-sessionid", description: "Disable SSL session-ID reusing" }, + { + name: "--noproxy", + description: "List of hosts which do not use proxy", + args: { name: "no-proxy-list" }, + }, + { name: "--ntlm", description: "Use HTTP NTLM authentication" }, + { + name: "--ntlm-wb", + description: "Use HTTP NTLM authentication with winbind", + }, + { + name: "--oauth2-bearer", + description: "OAuth 2 Bearer Token", + args: { name: "token" }, + }, + { + name: "--pass", + description: "Pass phrase for the private key", + args: { name: "phrase" }, + }, + { + name: "--path-as-is", + description: "Do not squash .. sequences in URL path", + }, + { + name: "--pinnedpubkey", + description: "FILE/HASHES Public key to verify peer against", + args: { name: "hashes" }, + }, + { + name: "--post301", + description: "Do not switch to GET after following a 301", + }, + { + name: "--post302", + description: "Do not switch to GET after following a 302", + }, + { + name: "--post303", + description: "Do not switch to GET after following a 303", + }, + { + name: "--preproxy", + description: "[protocol://]host[:port] Use this proxy first", + }, + { + name: "--proto", + description: "Enable/disable PROTOCOLS", + args: { name: "protocols" }, + }, + { + name: "--proto-default", + description: "Use PROTOCOL for any URL missing a scheme", + args: { name: "protocol" }, + }, + { + name: "--proto-redir", + description: "Enable/disable PROTOCOLS on redirect", + args: { name: "protocols" }, + }, + { + name: "--proxy-anyauth", + description: "Pick any proxy authentication method", + }, + { + name: "--proxy-basic", + description: "Use Basic authentication on the proxy", + }, + { + name: "--proxy-cacert", + description: "CA certificate to verify peer against for proxy", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--proxy-capath", + description: "CA directory to verify peer against for proxy", + args: { name: "dir", template: "folders" }, + }, + { + name: "--proxy-cert", + description: "Set client certificate for proxy", + args: { name: "cert[:passwd]" }, + }, + { + name: "--proxy-cert-type", + description: "Client certificate type for HTTPS proxy", + args: { name: "type" }, + }, + { + name: "--proxy-ciphers", + description: "SSL ciphers to use for proxy", + args: { name: "list" }, + }, + { + name: "--proxy-crlfile", + description: "Set a CRL list for proxy", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--proxy-digest", + description: "Use Digest authentication on the proxy", + }, + { + name: "--proxy-header", + description: "Pass custom header(s) to proxy", + args: { + name: "header/file", + suggestions: [ + { name: "Content-Type: application/json" }, + { name: "Content-Type: application/x-www-form-urlencoded" }, + ], + }, + }, + { + name: "--proxy-insecure", + description: "Do HTTPS proxy connections without verifying the proxy", + }, + { + name: "--proxy-key", + description: "Private key for HTTPS proxy", + args: { name: "key" }, + }, + { + name: "--proxy-key-type", + description: "Private key file type for proxy", + args: { name: "type" }, + }, + { + name: "--proxy-negotiate", + description: "Use HTTP Negotiate (SPNEGO) authentication on the proxy", + }, + { + name: "--proxy-ntlm", + description: "Use NTLM authentication on the proxy", + }, + { + name: "--proxy-pass", + description: "Pass phrase for the private key for HTTPS proxy", + args: { name: "phrase" }, + }, + { + name: "--proxy-pinnedpubkey", + description: "FILE/HASHES public key to verify proxy with", + args: { name: "hashes" }, + }, + { + name: "--proxy-service-name", + description: "SPNEGO proxy service name", + args: { name: "name" }, + }, + { + name: "--proxy-ssl-allow-beast", + description: "Allow security flaw for interop for HTTPS proxy", + }, + { + name: "--proxy-tls13-ciphers", + description: "List> TLS 1.3 proxy cipher suites", + args: { name: "ciphersuite" }, + }, + { + name: "--proxy-tlsauthtype", + description: "TLS authentication type for HTTPS proxy", + args: { name: "type" }, + }, + { + name: "--proxy-tlspassword", + description: "TLS password for HTTPS proxy", + args: { name: "string" }, + }, + { + name: "--proxy-tlsuser", + description: "TLS username for HTTPS proxy", + args: { name: "name" }, + }, + { name: "--proxy-tlsv1", description: "Use TLSv1 for HTTPS proxy" }, + { + name: "--proxy1.0", + description: "Use HTTP/1.0 proxy on given port", + args: { name: "host[:port]" }, + }, + { + name: "--pubkey", + description: "SSH Public key file name", + args: { name: "key", template: "filepaths" }, + }, + { + name: "--random-file", + description: "File for reading random data from", + args: { name: "file", template: "filepaths" }, + }, + { name: "--raw", description: 'Do HTTP "raw"; no transfer decoding' }, + { + name: "--remote-name-all", + description: "Use the remote file name for all URLs", + }, + { + name: "--request-target", + description: "Specify the target for this request", + }, + { + name: "--resolve", + description: "Resolve the host+port to this address", + args: { name: "host:port:address[,address]..." }, + }, + { + name: "--retry", + description: "Retry request if transient problems occur", + args: { name: "num" }, + }, + { + name: "--retry-connrefused", + description: "Retry on connection refused (use with --retry)", + }, + { + name: "--retry-delay", + description: "Wait time between retries", + args: { name: "seconds" }, + }, + { + name: "--retry-max-time", + description: "Retry only within this period", + args: { name: "seconds" }, + }, + { + name: "--sasl-ir", + description: "Enable initial response in SASL authentication", + }, + { + name: "--service-name", + description: "SPNEGO service name", + args: { name: "name" }, + }, + { + name: "--socks4", + description: "SOCKS4 proxy on given host + port", + args: { name: "host[:port]" }, + }, + { + name: "--socks4a", + description: "SOCKS4a proxy on given host + port", + args: { name: "host[:port]" }, + }, + { + name: "--socks5", + description: "SOCKS5 proxy on given host + port", + args: { name: "host[:port]" }, + }, + { + name: "--socks5-basic", + description: "Enable username/password auth for SOCKS5 proxies", + }, + { + name: "--socks5-gssapi", + description: "Enable GSS-API auth for SOCKS5 proxies", + }, + { + name: "--socks5-gssapi-nec", + description: "Compatibility with NEC SOCKS5 server", + }, + { + name: "--socks5-gssapi-service", + description: "SOCKS5 proxy service name for GSS-API", + args: { name: "name" }, + }, + { + name: "--socks5-hostname", + description: "SOCKS5 proxy, pass host name to proxy", + args: { name: "host[:port]" }, + }, + { name: "--ssl", description: "Try SSL/TLS" }, + { + name: "--ssl-auto-client-cert", + description: "Obtain and use a client certificate automatically", + }, + { + name: "--ssl-allow-beast", + description: "Allow security flaw to improve interop", + }, + { + name: "--ssl-no-revoke", + description: "Disable cert revocation checks (Schannel)", + }, + { name: "--ssl-reqd", description: "Require SSL/TLS" }, + { name: "--stderr", description: "Where to redirect stderr" }, + { + name: "--styled-output", + description: "Enable styled output for HTTP headers", + }, + { + name: "--suppress-connect-headers", + description: "Suppress proxy CONNECT response headers", + }, + { name: "--tcp-fastopen", description: "Use TCP Fast Open" }, + { name: "--tcp-nodelay", description: "Use the TCP_NODELAY option" }, + { + name: "--tftp-blksize", + description: "Set TFTP BLKSIZE option", + args: { name: "value" }, + }, + { name: "--tftp-no-options", description: "Do not send any TFTP options" }, + { + name: "--tls-max", + description: "Set maximum allowed TLS version", + args: { name: "VERSION" }, + }, + { + name: "--tls13-ciphers", + description: "Of TLS 1.3 ciphersuites> TLS 1.3 cipher suites to use", + args: { name: "list" }, + }, + { + name: "--tlsauthtype", + description: "TLS authentication type", + args: { name: "type" }, + }, + { name: "--tlspassword", description: "TLS password" }, + { name: "--tlsuser", description: "TLS user name", args: { name: "name" } }, + { name: "--tlsv1.0", description: "Use TLSv1.0 or greater" }, + { name: "--tlsv1.1", description: "Use TLSv1.1 or greater" }, + { name: "--tlsv1.2", description: "Use TLSv1.2 or greater" }, + { name: "--tlsv1.3", description: "Use TLSv1.3 or greater" }, + { + name: "--tr-encoding", + description: "Request compressed transfer encoding", + }, + { + name: "--trace", + description: "Write a debug trace to FILE", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--trace-ascii", + description: "Like --trace, but without hex output", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--trace-time", + description: "Add time stamps to trace/verbose output", + }, + { + name: "--unix-socket", + description: "Connect through this Unix domain socket", + args: { name: "path" }, + }, + { name: "--url", description: "URL to work with", args: { name: "url" } }, + { + name: "--xattr", + description: "Store metadata in extended file attributes", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/df.ts b/extensions/terminal-suggest/src/completions/upstream/df.ts index e7e005464ea..42f577d89e7 100644 --- a/extensions/terminal-suggest/src/completions/upstream/df.ts +++ b/extensions/terminal-suggest/src/completions/upstream/df.ts @@ -1,65 +1,65 @@ const completionSpec: Fig.Spec = { - name: "df", - description: "Display free disk space", - args: { - name: "file or filesystem", - }, - options: [ - { - name: "-a", - description: "Show all mount points", - }, - { - name: ["-b", "-P"], - description: "Use 512-byte blocks (default)", - exclusiveOn: ["-g", "-k", "-m"], - }, - { - name: "-g", - description: "Use 1073741824-byte (1-Gbyte) blocks", - exclusiveOn: ["-b", "-P", "-m", "-k"], - }, - { - name: "-m", - description: "Use 1048576-byte (1-Mbyte) blocks", - exclusiveOn: ["-b", "-P", "-g", "-k"], - }, - { - name: "-k", - description: "Use 1024-byte (1-Kbyte) blocks", - exclusiveOn: ["-b", "-P", "-g", "-m"], - }, - { - name: "-H", - description: '"Human-readable" output, uses base 10 unit suffixes', - exclusiveOn: ["-h"], - }, - { - name: "-h", - description: '"Human-readable" output, uses base 2 unit suffixes', - exclusiveOn: ["-H"], - }, - { - name: "-i", - description: "Include the number of free inodes", - }, - { - name: "-l", - description: "Only display information about locally-mounted filesystems", - }, - { - name: "-n", - description: "Print out the previously obtained statistics", - }, - { - name: "-T", - description: - "Only print out statistics for filesystems of the specified types (comma separated)", - args: { - name: "filesystem", - }, - }, - ], + name: "df", + description: "Display free disk space", + args: { + name: "file or filesystem", + }, + options: [ + { + name: "-a", + description: "Show all mount points", + }, + { + name: ["-b", "-P"], + description: "Use 512-byte blocks (default)", + exclusiveOn: ["-g", "-k", "-m"], + }, + { + name: "-g", + description: "Use 1073741824-byte (1-Gbyte) blocks", + exclusiveOn: ["-b", "-P", "-m", "-k"], + }, + { + name: "-m", + description: "Use 1048576-byte (1-Mbyte) blocks", + exclusiveOn: ["-b", "-P", "-g", "-k"], + }, + { + name: "-k", + description: "Use 1024-byte (1-Kbyte) blocks", + exclusiveOn: ["-b", "-P", "-g", "-m"], + }, + { + name: "-H", + description: '"Human-readable" output, uses base 10 unit suffixes', + exclusiveOn: ["-h"], + }, + { + name: "-h", + description: '"Human-readable" output, uses base 2 unit suffixes', + exclusiveOn: ["-H"], + }, + { + name: "-i", + description: "Include the number of free inodes", + }, + { + name: "-l", + description: "Only display information about locally-mounted filesystems", + }, + { + name: "-n", + description: "Print out the previously obtained statistics", + }, + { + name: "-T", + description: + "Only print out statistics for filesystems of the specified types (comma separated)", + args: { + name: "filesystem", + }, + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/du.ts b/extensions/terminal-suggest/src/completions/upstream/du.ts index 833fa52630f..ff3d6e421d2 100644 --- a/extensions/terminal-suggest/src/completions/upstream/du.ts +++ b/extensions/terminal-suggest/src/completions/upstream/du.ts @@ -1,93 +1,93 @@ const completionSpec: Fig.Spec = { - name: "du", - description: "Display disk usage statistics", - options: [ - { - name: "-a", - description: "Display an entry for each file in a file hierarchy", - exclusiveOn: ["-s", "-d"], - }, - { - name: "-c", - description: "Display a grand total", - }, - { - name: "-H", - description: - "Symbolic links on the command line are followed, symbolic links in file hierarchies are not followed", - exclusiveOn: ["-L", "-P"], - }, - { - name: "-h", - description: - '"Human-readable" output. Use unit suffixes: Byte, Kilobyte, Megabyte, Gigabyte, Terabyte and Petabyte', - exclusiveOn: ["-k", "-m", "-g"], - }, - { - name: "-g", - description: "Display block counts in 1073741824-byte (1-Gbyte) blocks", - exclusiveOn: ["-k", "-m", "-h"], - }, - { - name: "-k", - description: "Display block counts in 1024-byte (1-Kbyte) blocks", - exclusiveOn: ["-g", "-m", "-h"], - }, - { - name: "-m", - description: "Display block counts in 1048576-byte (1-Mbyte) blocks", - exclusiveOn: ["-g", "-k", "-h"], - }, - { - name: "-I", - description: "Ignore files and directories matching the specified mask", - args: { - name: "mask", - }, - }, - { - name: "-L", - description: - "Symbolic links on the command line and in file hierarchies are followed", - exclusiveOn: ["-H", "-P"], - }, - { - name: "-r", - description: - "Generate messages about directories that cannot be read, files that cannot be opened, and so on. This is the default case. This option exists solely for conformance with X/Open Portability Guide Issue 4 (``XPG4'')", - }, - { - name: "-P", - description: "No symbolic links are followed. This is the default", - exclusiveOn: ["-H", "-L"], - }, - { - name: "-d", - description: - "Display an entry for all files and directories depth directories deep", - exclusiveOn: ["-a", "-s"], - args: { - name: "depth", - suggestions: ["0", "1", "2"], - }, - }, - { - name: "-s", - description: - "Display an entry for each specified file. (Equivalent to -d 0)", - exclusiveOn: ["-a", "-d"], - }, - { - name: "-x", - description: - "Display an entry for each specified file. (Equivalent to -d 0)", - }, - ], - args: { - isOptional: true, - name: "files", - isVariadic: true, - template: ["filepaths", "folders"], - }, + name: "du", + description: "Display disk usage statistics", + options: [ + { + name: "-a", + description: "Display an entry for each file in a file hierarchy", + exclusiveOn: ["-s", "-d"], + }, + { + name: "-c", + description: "Display a grand total", + }, + { + name: "-H", + description: + "Symbolic links on the command line are followed, symbolic links in file hierarchies are not followed", + exclusiveOn: ["-L", "-P"], + }, + { + name: "-h", + description: + '"Human-readable" output. Use unit suffixes: Byte, Kilobyte, Megabyte, Gigabyte, Terabyte and Petabyte', + exclusiveOn: ["-k", "-m", "-g"], + }, + { + name: "-g", + description: "Display block counts in 1073741824-byte (1-Gbyte) blocks", + exclusiveOn: ["-k", "-m", "-h"], + }, + { + name: "-k", + description: "Display block counts in 1024-byte (1-Kbyte) blocks", + exclusiveOn: ["-g", "-m", "-h"], + }, + { + name: "-m", + description: "Display block counts in 1048576-byte (1-Mbyte) blocks", + exclusiveOn: ["-g", "-k", "-h"], + }, + { + name: "-I", + description: "Ignore files and directories matching the specified mask", + args: { + name: "mask", + }, + }, + { + name: "-L", + description: + "Symbolic links on the command line and in file hierarchies are followed", + exclusiveOn: ["-H", "-P"], + }, + { + name: "-r", + description: + "Generate messages about directories that cannot be read, files that cannot be opened, and so on. This is the default case. This option exists solely for conformance with X/Open Portability Guide Issue 4 (``XPG4'')", + }, + { + name: "-P", + description: "No symbolic links are followed. This is the default", + exclusiveOn: ["-H", "-L"], + }, + { + name: "-d", + description: + "Display an entry for all files and directories depth directories deep", + exclusiveOn: ["-a", "-s"], + args: { + name: "depth", + suggestions: ["0", "1", "2"], + }, + }, + { + name: "-s", + description: + "Display an entry for each specified file. (Equivalent to -d 0)", + exclusiveOn: ["-a", "-d"], + }, + { + name: "-x", + description: + "Display an entry for each specified file. (Equivalent to -d 0)", + }, + ], + args: { + isOptional: true, + name: "files", + isVariadic: true, + template: ["filepaths", "folders"], + }, }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/echo.ts b/extensions/terminal-suggest/src/completions/upstream/echo.ts index 8ca21b858b3..7b1452e2331 100644 --- a/extensions/terminal-suggest/src/completions/upstream/echo.ts +++ b/extensions/terminal-suggest/src/completions/upstream/echo.ts @@ -1,42 +1,42 @@ const environmentVariableGenerator: Fig.Generator = { - custom: async (tokens, _, context) => { - if (tokens.length < 3 || tokens[tokens.length - 1].startsWith("$")) { - return Object.keys(context.environmentVariables).map((suggestion) => ({ - name: `$${suggestion}`, - type: "arg", - description: "Environment Variable", - })); - } else { - return []; - } - }, - trigger: "$", + custom: async (tokens, _, context) => { + if (tokens.length < 3 || tokens[tokens.length - 1].startsWith("$")) { + return Object.keys(context.environmentVariables).map((suggestion) => ({ + name: `$${suggestion}`, + type: "arg", + description: "Environment Variable", + })); + } else { + return []; + } + }, + trigger: "$", }; const completionSpec: Fig.Spec = { - name: "echo", - description: "Write arguments to the standard output", - args: { - name: "string", - isVariadic: true, - optionsCanBreakVariadicArg: false, - suggestCurrentToken: true, - generators: environmentVariableGenerator, - }, - options: [ - { - name: "-n", - description: "Do not print the trailing newline character", - }, - { - name: "-e", - description: "Interpret escape sequences", - }, - { - name: "-E", - description: "Disable escape sequences", - }, - ], + name: "echo", + description: "Write arguments to the standard output", + args: { + name: "string", + isVariadic: true, + optionsCanBreakVariadicArg: false, + suggestCurrentToken: true, + generators: environmentVariableGenerator, + }, + options: [ + { + name: "-n", + description: "Do not print the trailing newline character", + }, + { + name: "-e", + description: "Interpret escape sequences", + }, + { + name: "-E", + description: "Disable escape sequences", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/find.ts b/extensions/terminal-suggest/src/completions/upstream/find.ts index 5b5245cb5fd..08b4d6a5393 100644 --- a/extensions/terminal-suggest/src/completions/upstream/find.ts +++ b/extensions/terminal-suggest/src/completions/upstream/find.ts @@ -1,71 +1,71 @@ const completionSpec: Fig.Spec = { - name: "find", - description: "Walk a file hierarchy", - args: [ - { - name: "path", - isOptional: true, - isVariadic: true, - template: ["folders"], - }, - { - // TODO Suggestions for primaries and operands. See `man find` - name: "expression", - description: "Composition of primaries and operands", - isOptional: true, - isVariadic: true, - }, - ], - options: [ - { - name: "-E", - description: - "Interpret regular expressions followed by -regex and -iregex primaries as extended", - }, - { - name: "-H", - description: - "Cause the file information and file type returned for each symbolic link specified to be those referenced by the link", - exclusiveOn: ["-L", "-P"], - }, - { - name: "-L", - description: - "Cause the file information and file type returned for each symbolic link to be those of the file referenced by the link", - exclusiveOn: ["-H", "-P"], - }, - { - name: "-P", - description: - "Cause the file information and file type returned for each symbolic link to be those for the link itself", - exclusiveOn: ["-H", "-L"], - }, - { - name: "-X", - description: "Permit find to be safely used in conjunction with xargs", - }, - { - name: "-d", - description: "Cause find to perform a depth-first traversal", - }, - { - name: "-f", - description: "Specify a file hierarch for find to traverse", - args: { - name: "path", - }, - }, - { - name: "-s", - description: - "Cause find to traverse the file hierarchies in lexicographical order", - }, - { - name: "-x", - description: - "Prevent find from descending into directories that have a device number different than that of the file from which the descent began", - }, - ], + name: "find", + description: "Walk a file hierarchy", + args: [ + { + name: "path", + isOptional: true, + isVariadic: true, + template: ["folders"], + }, + { + // TODO Suggestions for primaries and operands. See `man find` + name: "expression", + description: "Composition of primaries and operands", + isOptional: true, + isVariadic: true, + }, + ], + options: [ + { + name: "-E", + description: + "Interpret regular expressions followed by -regex and -iregex primaries as extended", + }, + { + name: "-H", + description: + "Cause the file information and file type returned for each symbolic link specified to be those referenced by the link", + exclusiveOn: ["-L", "-P"], + }, + { + name: "-L", + description: + "Cause the file information and file type returned for each symbolic link to be those of the file referenced by the link", + exclusiveOn: ["-H", "-P"], + }, + { + name: "-P", + description: + "Cause the file information and file type returned for each symbolic link to be those for the link itself", + exclusiveOn: ["-H", "-L"], + }, + { + name: "-X", + description: "Permit find to be safely used in conjunction with xargs", + }, + { + name: "-d", + description: "Cause find to perform a depth-first traversal", + }, + { + name: "-f", + description: "Specify a file hierarch for find to traverse", + args: { + name: "path", + }, + }, + { + name: "-s", + description: + "Cause find to traverse the file hierarchies in lexicographical order", + }, + { + name: "-x", + description: + "Prevent find from descending into directories that have a device number different than that of the file from which the descent began", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/git.ts b/extensions/terminal-suggest/src/completions/upstream/git.ts index 62d90d636b4..56468610acc 100644 --- a/extensions/terminal-suggest/src/completions/upstream/git.ts +++ b/extensions/terminal-suggest/src/completions/upstream/git.ts @@ -1,4 +1,4 @@ -// import { ai } from "../../fig/ai/ai"; +function ai(...args: any[]): undefined { return undefined; } const filterMessages = (out: string): string => { return out.startsWith("warning:") || out.startsWith("error:") @@ -29,7 +29,7 @@ const postProcessTrackedFiles: Fig.Generator["postProcess"] = ( try { ext = file.split(".").slice(-1)[0]; - } catch (e) { } + } catch (e) {} if (file.endsWith("/")) { ext = "folder"; @@ -53,69 +53,67 @@ interface PostProcessBranchesOptions { const postProcessBranches = (options: PostProcessBranchesOptions = {}): Fig.Generator["postProcess"] => - (out): (Fig.Suggestion | null)[] => { - const { insertWithoutRemotes = false } = options; + (out) => { + const { insertWithoutRemotes = false } = options; - const output = filterMessages(out); + const output = filterMessages(out); - if (output.startsWith("fatal:")) { - return []; - } + if (output.startsWith("fatal:")) { + return []; + } - const seen = new Set(); - return output - .split("\n") - .filter((line) => !line.trim().startsWith("HEAD")) - .map((branch) => { - let name = branch.trim(); - const parts = branch.match(/\S+/g); - if (!parts) { - return null; - } - if (parts.length > 1) { - if (parts[0] === "*") { - // We are in a detached HEAD state - if (branch.includes("HEAD detached")) { - return null; - } - // Current branch - return { - name: branch.replace("*", "").trim(), - description: "Current branch", - priority: 100, - icon: "⭐️", - }; - } else if (parts[0] === "+") { - // Branch checked out in another worktree. - name = branch.replace("+", "").trim(); + const seen = new Set(); + return output + .split("\n") + .filter((line) => !line.trim().startsWith("HEAD")) + .map((branch) => { + let name = branch.trim(); + const parts = branch.match(/\S+/g); + if (parts && parts.length > 1) { + if (parts[0] === "*") { + // We are in a detached HEAD state + if (branch.includes("HEAD detached")) { + return null; } + // Current branch + return { + name: branch.replace("*", "").trim(), + description: "Current branch", + priority: 100, + icon: "⭐️", + }; + } else if (parts[0] === "+") { + // Branch checked out in another worktree. + name = branch.replace("+", "").trim(); } + } - let description = "Branch"; + let description = "Branch"; - if (insertWithoutRemotes && name.startsWith("remotes/")) { - name = name.slice(name.indexOf("/", 8) + 1); - description = "Remote branch"; - } + if (insertWithoutRemotes && name.startsWith("remotes/")) { + name = name.slice(name.indexOf("/", 8) + 1); + description = "Remote branch"; + } - const space = name.indexOf(" "); - if (space !== -1) { - name = name.slice(0, space); - } + const space = name.indexOf(" "); + if (space !== -1) { + name = name.slice(0, space); + } - return { - name, - description, - icon: "fig://icon?type=git", - priority: 75, - }; - }) - .filter((suggestion) => { - if (!suggestion || seen.has(suggestion.name)) return false; - seen.add(suggestion.name); - return true; - }); - }; + return { + name, + description, + icon: "fig://icon?type=git", + priority: 75, + }; + }) + .filter((suggestion) => { + if (!suggestion) return false; + if (seen.has(suggestion.name)) return false; + seen.add(suggestion.name); + return true; + }); + }; export const gitGenerators: Record = { // Commit history @@ -298,14 +296,16 @@ export const gitGenerators: Record = { remotes: { script: ["git", "--no-optional-locks", "remote", "-v"], postProcess: function (out) { - const remoteURLs = out.split("\n").reduce>((dict, line) => { - const pair = line.split("\t"); - const remote = pair[0]; - const url = pair[1].split(" ")[0]; + const remoteURLs = out + .split("\n") + .reduce>((dict, line) => { + const pair = line.split("\t"); + const remote = pair[0]; + const url = pair[1].split(" ")[0]; - dict[remote] = url; - return dict; - }, {}); + dict[remote] = url; + return dict; + }, {}); return Object.keys(remoteURLs).map((remote) => { const url = remoteURLs[remote]; @@ -426,7 +426,7 @@ export const gitGenerators: Record = { let ext = ""; try { ext = file.split(".").slice(-1)[0]; - } catch (e) { } + } catch (e) {} if (file.endsWith("/")) { ext = "folder"; @@ -4026,7 +4026,7 @@ const daemonServices: Fig.Suggestion[] = [ const completionSpec: Fig.Spec = { name: "git", - description: "Distributed version control system", + description: "The stupid content tracker", generateSpec: async (_, executeShellCommand) => { const { stdout } = await executeShellCommand({ command: "git", @@ -4401,35 +4401,34 @@ const completionSpec: Fig.Spec = { description: "Use the given message as the commit message", args: { name: "message", - // generators: ai({ - // name: "git commit -m", - // prompt: async ({ executeCommand }) => { - // const { stdout } = await executeCommand({ - // command: "git", - // args: [ - // "log", - // "--pretty=format:%s", - // "--abbrev-commit", - // "--max-count=20", - // ], - // }); + generators: ai({ + name: "git commit -m", + prompt: async ({ executeCommand }: any) => { + const { stdout } = await executeCommand({ + command: "git", + args: [ + "log", + "--pretty=format:%s", + "--abbrev-commit", + "--max-count=20", + ], + }); - // return ( - // 'Generate a git commit message summary based on this git diff, the "summary" must be no more ' + - // "than 70-75 characters, and it must describe both what the patch changes, as well as why the " + - // `patch might be necessary.\n\nHere are some examples from the repo:\n${stdout}` - // ); - // }, - // message: async ({ executeCommand }) => - // ( - // await executeCommand({ - // command: "git", - // args: ["diff", "--staged"], - // }) - // ).stdout, - // splitOn: "\n", - // }), - // }, + return ( + 'Generate a git commit message summary based on this git diff, the "summary" must be no more ' + + "than 70-75 characters, and it must describe both what the patch changes, as well as why the " + + `patch might be necessary.\n\nHere are some examples from the repo:\n${stdout}` + ); + }, + message: async ({ executeCommand }: any) => + ( + await executeCommand({ + command: "git", + args: ["diff", "--staged"], + }) + ).stdout, + splitOn: "\n", + }), }, }, { diff --git a/extensions/terminal-suggest/src/completions/upstream/grep.ts b/extensions/terminal-suggest/src/completions/upstream/grep.ts index a7c15281424..a897d463a0b 100644 --- a/extensions/terminal-suggest/src/completions/upstream/grep.ts +++ b/extensions/terminal-suggest/src/completions/upstream/grep.ts @@ -1,344 +1,344 @@ const completionSpec: Fig.Spec = { - name: "grep", - description: - "Matches patterns in input text. Supports simple patterns and regular expressions", - args: [ - { - name: "search pattern", - suggestCurrentToken: true, - }, - { - name: "file", - template: "filepaths", - }, - ], - options: [ - { - name: "--help", - description: - "Print a usage message briefly summarizing these command-line options and the bug-reporting address, then exit", - }, - { - name: ["-E", "--extended-regexp"], - description: - "Interpret PATTERN as an extended regular expression (-E is specified by POSIX.)", - }, - { - name: ["-F", "--fixed-string"], - description: - "Interpret PATTERN as a list of fixed strings, separated by newlines, any of which is to be matched. (-F is specified by POSIX.)", - }, - { - name: ["-G", "--basic-regexp"], - description: - "Interpret PATTERN as a basic regular expression (BRE, see below). This is the default", - }, - { - name: ["-e", "--regexp"], - description: - "Use PATTERN as the pattern. This can be used to specify multiple search patterns, or to protect a pattern beginning with a hyphen (-). (-e is specified by POSIX.)", - args: { - name: "pattern", - }, - }, - { - name: ["-i", "--ignore-case", "-y"], - description: - "Ignore case distinctions in both the PATTERN and the input files. (-i is specified by POSIX.)", - }, - { - name: ["-v", "--invert-match"], - description: - "Invert the sense of matching, to select non-matching lines. (-v is specified by POSIX.)", - }, - { - name: ["-w", "--word-regexp"], - description: - "Select only those lines containing matches that form whole words. The test is that the matching substring must either be at the beginning of the line, or preceded by a non-word constituent character. Similarly, it must be either at the end of the line or followed by a non-word constituent character. Word-constituent characters are letters, digits, and the underscore", - }, - { - name: ["-x", "--line-regexp"], - description: - "Select only those matches that exactly match the whole line. (-x is specified by POSIX.)", - }, - { - name: ["-c", "--count"], - description: - "Suppress normal output; instead print a count of matching lines for each input file. With the -v, --invert-match option, count non-matching lines. (-c is specified by POSIX.)", - }, - { - name: "--color", - description: - "Surround the matched (non-empty) strings, matching lines, context lines, file names, line numbers, byte offsets, and separators (for fields and groups of context lines) with escape sequences to display them in color on the terminal. The colors are defined by the environment variable GREP_COLORS. The deprecated environment variable GREP_COLOR is still supported, but its setting does not have priority", - args: { - name: "WHEN", - default: "auto", - suggestions: ["never", "always", "auto"], - }, - }, - { - name: ["-L", "--files-without-match"], - exclusiveOn: ["-l", "--files-with-matches"], - description: - "Suppress normal output; instead print the name of each input file from which no output would normally have been printed. The scanning will stop on the first match", - }, - { - name: ["-l", "--files-with-matches"], - exclusiveOn: ["-L", "--files-without-match"], - description: - "Suppress normal output; instead print the name of each input file from which output would normally have been printed. The scanning will stop on the first match. (-l is specified by POSIX.)", - }, - { - name: ["-m", "--max-count"], - description: - "Stop reading a file after NUM matching lines. If the input is standard input from a regular file, and NUM matching lines are output, grep ensures that the standard input is positioned to just after the last matching line before exiting, regardless of the presence of trailing context lines. This enables a calling process to resume a search. When grep stops after NUM matching lines, it outputs any trailing context lines. When the -c or --count option is also used, grep does not output a count greater than NUM. When the -v or --invert-match option is also used, grep stops after outputting NUM non-matching lines", - args: { - name: "NUM", - }, - }, - { - name: ["-o", "--only-matching"], - description: - "Print only the matched (non-empty) parts of a matching line, with each such part on a separate output line", - }, - { - name: ["-q", "--quiet", "--silent"], - description: - "Quiet; do not write anything to standard output. Exit immediately with zero status if any match is found, even if an error was detected. Also see the -s or --no-messages option. (-q is specified by POSIX.)", - }, - { - name: ["-s", "--no-messages"], - description: - "Suppress error messages about nonexistent or unreadable files. Portability note: unlike GNU grep, 7th Edition Unix grep did not conform to POSIX, because it lacked -q and its -s option behaved like GNU grep's -q option. USG -style grep also lacked -q but its -s option behaved like GNU grep. Portable shell scripts should avoid both -q and -s and should redirect standard and error output to /dev/null instead. (-s is specified by POSIX.)", - }, - { - name: ["-b", "--byte-offset"], - description: - "Print the 0-based byte offset within the input file before each line of output. If -o (--only-matching) is specified, print the offset of the matching part itself", - }, - { - name: ["-H", "--with-filename"], - description: - "Print the file name for each match. This is the default when there is more than one file to search", - }, - { - name: ["-h", "--no-filename"], - description: - "Suppress the prefixing of file names on output. This is the default when there is only one file (or only standard input) to search", - }, - { - name: "--label", - description: - "Display input actually coming from standard input as input coming from file LABEL. This is especially useful when implementing tools like zgrep, e.g., gzip -cd foo.gz | grep --label=foo -H something", - args: { - name: "LABEL", - }, - }, - { - name: ["-n", "--line-number"], - description: - "Prefix each line of output with the 1-based line number within its input file. (-n is specified by POSIX.)", - }, - { - name: ["-T", "--initial-tab"], - description: - "Make sure that the first character of actual line content lies on a tab stop, so that the alignment of tabs looks normal. This is useful with options that prefix their output to the actual content: -H,-n, and -b. In order to improve the probability that lines from a single file will all start at the same column, this also causes the line number and byte offset (if present) to be printed in a minimum size field width", - }, - { - name: ["-u", "--unix-byte-offsets"], - description: - "Report Unix-style byte offsets. This switch causes grep to report byte offsets as if the file were a Unix-style text file, i.e., with CR characters stripped off. This will produce results identical to running grep on a Unix machine. This option has no effect unless -b option is also used; it has no effect on platforms other than MS-DOS and MS -Windows", - }, - { - name: "--null", - description: - "Output a zero byte (the ASCII NUL character) instead of the character that normally follows a file name. For example, grep -lZ outputs a zero byte after each file name instead of the usual newline. This option makes the output unambiguous, even in the presence of file names containing unusual characters like newlines. This option can be used with commands like find -print0, perl -0, sort -z, and xargs -0 to process arbitrary file names, even those that contain newline characters", - }, - { - name: ["-A", "--after-context"], - description: "Print num lines of trailing context after each match", - args: { - name: "NUM", - }, - }, - { - name: ["-B", "--before-context"], - description: - "Print num lines of leading context before each match. See also the -A and -C options", - args: { - name: "NUM", - }, - }, - { - name: ["-C", "--context"], - description: - "Print NUM lines of output context. Places a line containing a group separator (--) between contiguous groups of matches. With the -o or --only-matching option, this has no effect and a warning is given", - args: { - name: "NUM", - }, - }, - { - name: ["-a", "--text"], - description: - "Treat all files as ASCII text. Normally grep will simply print ``Binary file ... matches'' if files contain binary characters. Use of this option forces grep to output lines matching the specified pattern", - }, - { - name: "--binary-files", - description: "Controls searching and printing of binary files", - args: { - name: "value", - default: "binary", - suggestions: [ - { - name: "binary", - description: "Search binary files but do not print them", - }, - { - name: "without-match", - description: "Do not search binary files", - }, - { - name: "text", - description: "Treat all files as text", - }, - ], - }, - }, - { - name: ["-D", "--devices"], - description: "Specify the demanded action for devices, FIFOs and sockets", - args: { - name: "action", - default: "read", - suggestions: [ - { - name: "read", - description: "Read as if they were normal files", - }, - { - name: "skip", - description: "Devices will be silently skipped", - }, - ], - }, - }, - { - name: ["-d", "--directories"], - description: "Specify the demanded action for directories", - args: { - name: "action", - default: "read", - suggestions: [ - { - name: "read", - description: - "Directories are read in the same manner as normal files", - }, - { - name: "skip", - description: "Silently ignore the directories", - }, - { - name: "recurse", - description: "Read directories recursively", - }, - ], - }, - }, - { - name: "--exclude", - description: - "Note that --exclude patterns take priority over --include patterns, and if no --include pattern is specified, all files are searched that are not excluded. Patterns are matched to the full path specified, not only to the filename component", - args: { - name: "GLOB", - isOptional: true, - }, - }, - { - name: "--exclude-dir", - description: - "If -R is specified, only directories matching the given filename pattern are searched. Note that --exclude-dir patterns take priority over --include-dir patterns", - isRepeatable: true, - args: { - name: "dir", - template: "folders", - isOptional: true, - }, - }, - { - name: "-I", - description: - "Ignore binary files. This option is equivalent to --binary-file=without-match option", - }, - { - name: "--include", - description: - "If specified, only files matching the given filename pattern are searched. Note that --exclude patterns take priority over --include patterns. Patterns are matched to the full path specified, not only to the filename component", - args: { - name: "GLOB", - isOptional: true, - }, - }, - { - name: "--include-dir", - description: - "If -R is specified, only directories matching the given filename pattern are searched. Note that --exclude-dir patterns take priority over --include-dir patterns", - args: { - name: "dir", - template: "folders", - isOptional: true, - }, - }, - { - name: ["-R", "-r", "--recursive"], - description: "Recursively search subdirectories listed", - }, - { - name: "--line-buffered", - description: - "Force output to be line buffered. By default, output is line buffered when standard output is a terminal and block buffered otherwise", - }, - { - name: ["-U", "--binary"], - description: "Search binary files, but do not attempt to print them", - }, - { - name: ["-J", "-bz2decompress"], - description: - "Decompress the bzip2(1) compressed file before looking for the text", - }, - { - name: ["-V", "--version"], - description: "Print version number of grep to the standard output stream", - }, - { - name: ["-P", "--perl-regexp"], - description: "Interpret pattern as a Perl regular expression", - }, - { - name: ["-f", "--file"], - description: - "Obtain patterns from FILE, one per line. The empty file contains zero patterns, and therefore matches nothing. (-f is specified by POSIX.)", - args: { - name: "FILE", - template: "filepaths", - }, - }, - ], - additionalSuggestions: [ - { - name: "-RIn", - description: - "Search for a pattern [R]ecursively in the current directory, showing matching line [n]umbers, [I]gnoring non-text files", - insertValue: "-RI{cursor}", - }, - { - name: "-Hn", - description: - "Print file name with the corresponding line number (n) for each match", - insertValue: "-H{cursor}", - }, - ], + name: "grep", + description: + "Matches patterns in input text. Supports simple patterns and regular expressions", + args: [ + { + name: "search pattern", + suggestCurrentToken: true, + }, + { + name: "file", + template: "filepaths", + }, + ], + options: [ + { + name: "--help", + description: + "Print a usage message briefly summarizing these command-line options and the bug-reporting address, then exit", + }, + { + name: ["-E", "--extended-regexp"], + description: + "Interpret PATTERN as an extended regular expression (-E is specified by POSIX.)", + }, + { + name: ["-F", "--fixed-string"], + description: + "Interpret PATTERN as a list of fixed strings, separated by newlines, any of which is to be matched. (-F is specified by POSIX.)", + }, + { + name: ["-G", "--basic-regexp"], + description: + "Interpret PATTERN as a basic regular expression (BRE, see below). This is the default", + }, + { + name: ["-e", "--regexp"], + description: + "Use PATTERN as the pattern. This can be used to specify multiple search patterns, or to protect a pattern beginning with a hyphen (-). (-e is specified by POSIX.)", + args: { + name: "pattern", + }, + }, + { + name: ["-i", "--ignore-case", "-y"], + description: + "Ignore case distinctions in both the PATTERN and the input files. (-i is specified by POSIX.)", + }, + { + name: ["-v", "--invert-match"], + description: + "Invert the sense of matching, to select non-matching lines. (-v is specified by POSIX.)", + }, + { + name: ["-w", "--word-regexp"], + description: + "Select only those lines containing matches that form whole words. The test is that the matching substring must either be at the beginning of the line, or preceded by a non-word constituent character. Similarly, it must be either at the end of the line or followed by a non-word constituent character. Word-constituent characters are letters, digits, and the underscore", + }, + { + name: ["-x", "--line-regexp"], + description: + "Select only those matches that exactly match the whole line. (-x is specified by POSIX.)", + }, + { + name: ["-c", "--count"], + description: + "Suppress normal output; instead print a count of matching lines for each input file. With the -v, --invert-match option, count non-matching lines. (-c is specified by POSIX.)", + }, + { + name: "--color", + description: + "Surround the matched (non-empty) strings, matching lines, context lines, file names, line numbers, byte offsets, and separators (for fields and groups of context lines) with escape sequences to display them in color on the terminal. The colors are defined by the environment variable GREP_COLORS. The deprecated environment variable GREP_COLOR is still supported, but its setting does not have priority", + args: { + name: "WHEN", + default: "auto", + suggestions: ["never", "always", "auto"], + }, + }, + { + name: ["-L", "--files-without-match"], + exclusiveOn: ["-l", "--files-with-matches"], + description: + "Suppress normal output; instead print the name of each input file from which no output would normally have been printed. The scanning will stop on the first match", + }, + { + name: ["-l", "--files-with-matches"], + exclusiveOn: ["-L", "--files-without-match"], + description: + "Suppress normal output; instead print the name of each input file from which output would normally have been printed. The scanning will stop on the first match. (-l is specified by POSIX.)", + }, + { + name: ["-m", "--max-count"], + description: + "Stop reading a file after NUM matching lines. If the input is standard input from a regular file, and NUM matching lines are output, grep ensures that the standard input is positioned to just after the last matching line before exiting, regardless of the presence of trailing context lines. This enables a calling process to resume a search. When grep stops after NUM matching lines, it outputs any trailing context lines. When the -c or --count option is also used, grep does not output a count greater than NUM. When the -v or --invert-match option is also used, grep stops after outputting NUM non-matching lines", + args: { + name: "NUM", + }, + }, + { + name: ["-o", "--only-matching"], + description: + "Print only the matched (non-empty) parts of a matching line, with each such part on a separate output line", + }, + { + name: ["-q", "--quiet", "--silent"], + description: + "Quiet; do not write anything to standard output. Exit immediately with zero status if any match is found, even if an error was detected. Also see the -s or --no-messages option. (-q is specified by POSIX.)", + }, + { + name: ["-s", "--no-messages"], + description: + "Suppress error messages about nonexistent or unreadable files. Portability note: unlike GNU grep, 7th Edition Unix grep did not conform to POSIX, because it lacked -q and its -s option behaved like GNU grep's -q option. USG -style grep also lacked -q but its -s option behaved like GNU grep. Portable shell scripts should avoid both -q and -s and should redirect standard and error output to /dev/null instead. (-s is specified by POSIX.)", + }, + { + name: ["-b", "--byte-offset"], + description: + "Print the 0-based byte offset within the input file before each line of output. If -o (--only-matching) is specified, print the offset of the matching part itself", + }, + { + name: ["-H", "--with-filename"], + description: + "Print the file name for each match. This is the default when there is more than one file to search", + }, + { + name: ["-h", "--no-filename"], + description: + "Suppress the prefixing of file names on output. This is the default when there is only one file (or only standard input) to search", + }, + { + name: "--label", + description: + "Display input actually coming from standard input as input coming from file LABEL. This is especially useful when implementing tools like zgrep, e.g., gzip -cd foo.gz | grep --label=foo -H something", + args: { + name: "LABEL", + }, + }, + { + name: ["-n", "--line-number"], + description: + "Prefix each line of output with the 1-based line number within its input file. (-n is specified by POSIX.)", + }, + { + name: ["-T", "--initial-tab"], + description: + "Make sure that the first character of actual line content lies on a tab stop, so that the alignment of tabs looks normal. This is useful with options that prefix their output to the actual content: -H,-n, and -b. In order to improve the probability that lines from a single file will all start at the same column, this also causes the line number and byte offset (if present) to be printed in a minimum size field width", + }, + { + name: ["-u", "--unix-byte-offsets"], + description: + "Report Unix-style byte offsets. This switch causes grep to report byte offsets as if the file were a Unix-style text file, i.e., with CR characters stripped off. This will produce results identical to running grep on a Unix machine. This option has no effect unless -b option is also used; it has no effect on platforms other than MS-DOS and MS -Windows", + }, + { + name: "--null", + description: + "Output a zero byte (the ASCII NUL character) instead of the character that normally follows a file name. For example, grep -lZ outputs a zero byte after each file name instead of the usual newline. This option makes the output unambiguous, even in the presence of file names containing unusual characters like newlines. This option can be used with commands like find -print0, perl -0, sort -z, and xargs -0 to process arbitrary file names, even those that contain newline characters", + }, + { + name: ["-A", "--after-context"], + description: "Print num lines of trailing context after each match", + args: { + name: "NUM", + }, + }, + { + name: ["-B", "--before-context"], + description: + "Print num lines of leading context before each match. See also the -A and -C options", + args: { + name: "NUM", + }, + }, + { + name: ["-C", "--context"], + description: + "Print NUM lines of output context. Places a line containing a group separator (--) between contiguous groups of matches. With the -o or --only-matching option, this has no effect and a warning is given", + args: { + name: "NUM", + }, + }, + { + name: ["-a", "--text"], + description: + "Treat all files as ASCII text. Normally grep will simply print ``Binary file ... matches'' if files contain binary characters. Use of this option forces grep to output lines matching the specified pattern", + }, + { + name: "--binary-files", + description: "Controls searching and printing of binary files", + args: { + name: "value", + default: "binary", + suggestions: [ + { + name: "binary", + description: "Search binary files but do not print them", + }, + { + name: "without-match", + description: "Do not search binary files", + }, + { + name: "text", + description: "Treat all files as text", + }, + ], + }, + }, + { + name: ["-D", "--devices"], + description: "Specify the demanded action for devices, FIFOs and sockets", + args: { + name: "action", + default: "read", + suggestions: [ + { + name: "read", + description: "Read as if they were normal files", + }, + { + name: "skip", + description: "Devices will be silently skipped", + }, + ], + }, + }, + { + name: ["-d", "--directories"], + description: "Specify the demanded action for directories", + args: { + name: "action", + default: "read", + suggestions: [ + { + name: "read", + description: + "Directories are read in the same manner as normal files", + }, + { + name: "skip", + description: "Silently ignore the directories", + }, + { + name: "recurse", + description: "Read directories recursively", + }, + ], + }, + }, + { + name: "--exclude", + description: + "Note that --exclude patterns take priority over --include patterns, and if no --include pattern is specified, all files are searched that are not excluded. Patterns are matched to the full path specified, not only to the filename component", + args: { + name: "GLOB", + isOptional: true, + }, + }, + { + name: "--exclude-dir", + description: + "If -R is specified, only directories matching the given filename pattern are searched. Note that --exclude-dir patterns take priority over --include-dir patterns", + isRepeatable: true, + args: { + name: "dir", + template: "folders", + isOptional: true, + }, + }, + { + name: "-I", + description: + "Ignore binary files. This option is equivalent to --binary-file=without-match option", + }, + { + name: "--include", + description: + "If specified, only files matching the given filename pattern are searched. Note that --exclude patterns take priority over --include patterns. Patterns are matched to the full path specified, not only to the filename component", + args: { + name: "GLOB", + isOptional: true, + }, + }, + { + name: "--include-dir", + description: + "If -R is specified, only directories matching the given filename pattern are searched. Note that --exclude-dir patterns take priority over --include-dir patterns", + args: { + name: "dir", + template: "folders", + isOptional: true, + }, + }, + { + name: ["-R", "-r", "--recursive"], + description: "Recursively search subdirectories listed", + }, + { + name: "--line-buffered", + description: + "Force output to be line buffered. By default, output is line buffered when standard output is a terminal and block buffered otherwise", + }, + { + name: ["-U", "--binary"], + description: "Search binary files, but do not attempt to print them", + }, + { + name: ["-J", "-bz2decompress"], + description: + "Decompress the bzip2(1) compressed file before looking for the text", + }, + { + name: ["-V", "--version"], + description: "Print version number of grep to the standard output stream", + }, + { + name: ["-P", "--perl-regexp"], + description: "Interpret pattern as a Perl regular expression", + }, + { + name: ["-f", "--file"], + description: + "Obtain patterns from FILE, one per line. The empty file contains zero patterns, and therefore matches nothing. (-f is specified by POSIX.)", + args: { + name: "FILE", + template: "filepaths", + }, + }, + ], + additionalSuggestions: [ + { + name: "-RIn", + description: + "Search for a pattern [R]ecursively in the current directory, showing matching line [n]umbers, [I]gnoring non-text files", + insertValue: "-RI{cursor}", + }, + { + name: "-Hn", + description: + "Print file name with the corresponding line number (n) for each match", + insertValue: "-H{cursor}", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/head.ts b/extensions/terminal-suggest/src/completions/upstream/head.ts index bc837e09dad..a7ae0a7aede 100644 --- a/extensions/terminal-suggest/src/completions/upstream/head.ts +++ b/extensions/terminal-suggest/src/completions/upstream/head.ts @@ -1,35 +1,35 @@ const completionSpec: Fig.Spec = { - name: "head", - description: "Output the first part of files", - args: { - name: "file", - template: "filepaths", - }, - options: [ - { - name: ["-c", "--bytes"], - description: "Print the first [numBytes] bytes of each file", - args: { name: "numBytes" }, - }, - { - name: ["-n", "--lines"], - description: "Print the first [numLines] lines instead of the first 10", - args: { name: "numLines" }, - }, - { - name: ["-q", "--quiet", "--silent"], - description: "Never print headers giving file names", - }, - { - name: ["-v", "--verbose"], - description: "Always print headers giving file names", - }, - { name: "--help", description: "Display this help and exit" }, - { - name: "--version", - description: "Output version information and exit", - }, - ], + name: "head", + description: "Output the first part of files", + args: { + name: "file", + template: "filepaths", + }, + options: [ + { + name: ["-c", "--bytes"], + description: "Print the first [numBytes] bytes of each file", + args: { name: "numBytes" }, + }, + { + name: ["-n", "--lines"], + description: "Print the first [numLines] lines instead of the first 10", + args: { name: "numLines" }, + }, + { + name: ["-q", "--quiet", "--silent"], + description: "Never print headers giving file names", + }, + { + name: ["-v", "--verbose"], + description: "Always print headers giving file names", + }, + { name: "--help", description: "Display this help and exit" }, + { + name: "--version", + description: "Output version information and exit", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/killall.ts b/extensions/terminal-suggest/src/completions/upstream/killall.ts index c22e55dd6a4..c963a97177f 100644 --- a/extensions/terminal-suggest/src/completions/upstream/killall.ts +++ b/extensions/terminal-suggest/src/completions/upstream/killall.ts @@ -1,153 +1,153 @@ // Linux incompatible const signals = [ - "hup", - "int", - "quit", - "ill", - "trap", - "abrt", - "emt", - "fpe", - "kill", - "bus", - "segv", - "sys", - "pipe", - "alrm", - // This is the default signal - // "term", - "urg", - "stop", - "tstp", - "cont", - "chld", - "ttin", - "ttou", - "io", - "xcpu", - "xfsz", - "vtalrm", - "prof", - "winch", - "info", - "usr1", - "usr2", + "hup", + "int", + "quit", + "ill", + "trap", + "abrt", + "emt", + "fpe", + "kill", + "bus", + "segv", + "sys", + "pipe", + "alrm", + // This is the default signal + // "term", + "urg", + "stop", + "tstp", + "cont", + "chld", + "ttin", + "ttou", + "io", + "xcpu", + "xfsz", + "vtalrm", + "prof", + "winch", + "info", + "usr1", + "usr2", ]; const completionSpec: Fig.Spec = { - name: "killall", - description: "Kill processes by name", - args: { - name: "process_name", - isVariadic: true, - generators: { - // All processes, only display the path - script: ["bash", "-c", "ps -A -o comm | sort -u"], - postProcess: (out) => - out - .trim() - .split("\n") - .map((path) => { - const appExtIndex = path.indexOf(".app/"); - const isApp = appExtIndex !== -1; - const name = path.slice(path.lastIndexOf("/") + 1); - const nameChars = new Set(name); - const badChars = ["(", "_", "."]; - return { - name, - description: path, - priority: - !badChars.some((char) => nameChars.has(char)) && isApp - ? 51 - : 40, - icon: isApp - ? "fig://" + path.slice(0, appExtIndex + 4) - : "fig://icon?type=gear", - }; - }), - }, - }, - options: [ - { - name: "-d", - description: "Be verbose (dry run) and display number of user processes", - }, - { - name: "-e", - description: - "Use the effective user ID instead of the real user ID for matching processes with -u", - }, - { - name: "-help", - description: "Display help and exit", - }, - { - name: "-I", - description: "Request confirmation before killing each process", - }, - { - name: "-l", - description: "List the names of the available signals and exit", - }, - { - name: "-m", - description: "Match the process name as a regular expression", - }, - { - name: "-v", - description: "Be verbose", - }, - { - name: "-s", - description: "Be verbose (dry run)", - }, - ...signals.map((signal) => ({ - name: "-SIG" + signal.toUpperCase(), - description: `Send ${signal.toUpperCase()} instead of TERM`, - })), - { - name: "-u", - description: - "Limit potentially matching processes to those belonging to the user", - args: { - name: "user", - generators: { - script: ["bash", "-c", "dscl . -list /Users | grep -v '^_'"], - postProcess: (out) => - out - .trim() - .split("\n") - .map((username) => ({ - name: username, - icon: "fig://template?badge=👤", - })), - }, - }, - }, - { - name: "-t", - description: - "Limit matching processes to those running on the specified TTY", - args: { - name: "tty", - }, - }, - { - name: "-c", - description: "Limit matching processes to those matching the given name", - args: { - name: "name", - }, - }, - { - name: "-q", - description: "Suppress error message if no processes are matched", - }, - { - name: "-z", - description: "Do not skip zombies", - }, - ], + name: "killall", + description: "Kill processes by name", + args: { + name: "process_name", + isVariadic: true, + generators: { + // All processes, only display the path + script: ["bash", "-c", "ps -A -o comm | sort -u"], + postProcess: (out) => + out + .trim() + .split("\n") + .map((path) => { + const appExtIndex = path.indexOf(".app/"); + const isApp = appExtIndex !== -1; + const name = path.slice(path.lastIndexOf("/") + 1); + const nameChars = new Set(name); + const badChars = ["(", "_", "."]; + return { + name, + description: path, + priority: + !badChars.some((char) => nameChars.has(char)) && isApp + ? 51 + : 40, + icon: isApp + ? "fig://" + path.slice(0, appExtIndex + 4) + : "fig://icon?type=gear", + }; + }), + }, + }, + options: [ + { + name: "-d", + description: "Be verbose (dry run) and display number of user processes", + }, + { + name: "-e", + description: + "Use the effective user ID instead of the real user ID for matching processes with -u", + }, + { + name: "-help", + description: "Display help and exit", + }, + { + name: "-I", + description: "Request confirmation before killing each process", + }, + { + name: "-l", + description: "List the names of the available signals and exit", + }, + { + name: "-m", + description: "Match the process name as a regular expression", + }, + { + name: "-v", + description: "Be verbose", + }, + { + name: "-s", + description: "Be verbose (dry run)", + }, + ...signals.map((signal) => ({ + name: "-SIG" + signal.toUpperCase(), + description: `Send ${signal.toUpperCase()} instead of TERM`, + })), + { + name: "-u", + description: + "Limit potentially matching processes to those belonging to the user", + args: { + name: "user", + generators: { + script: ["bash", "-c", "dscl . -list /Users | grep -v '^_'"], + postProcess: (out) => + out + .trim() + .split("\n") + .map((username) => ({ + name: username, + icon: "fig://template?badge=👤", + })), + }, + }, + }, + { + name: "-t", + description: + "Limit matching processes to those running on the specified TTY", + args: { + name: "tty", + }, + }, + { + name: "-c", + description: "Limit matching processes to those matching the given name", + args: { + name: "name", + }, + }, + { + name: "-q", + description: "Suppress error message if no processes are matched", + }, + { + name: "-z", + description: "Do not skip zombies", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/less.ts b/extensions/terminal-suggest/src/completions/upstream/less.ts index f8994808a44..f50abbd4db8 100644 --- a/extensions/terminal-suggest/src/completions/upstream/less.ts +++ b/extensions/terminal-suggest/src/completions/upstream/less.ts @@ -1,19 +1,19 @@ const completionSpec: Fig.Spec = { - name: "less", - description: "Opposite of more", - args: { - isVariadic: true, - template: "filepaths", - }, - options: [ - { - name: ["-?", "--help"], - description: - 'This option displays a summary of the commands accepted by less (the same as the h command). (Depending on how your shell interprets the question mark, it may be necessary to quote the question mark, thus: "-?"', - }, - { - name: ["-a", "--search-skip-screen"], - description: `By default, forward searches start at the top of the displayed + name: "less", + description: "Opposite of more", + args: { + isVariadic: true, + template: "filepaths", + }, + options: [ + { + name: ["-?", "--help"], + description: + 'This option displays a summary of the commands accepted by less (the same as the h command). (Depending on how your shell interprets the question mark, it may be necessary to quote the question mark, thus: "-?"', + }, + { + name: ["-a", "--search-skip-screen"], + description: `By default, forward searches start at the top of the displayed screen and backwards searches start at the bottom of the displayed screen (except for repeated searches invoked by the n or N commands, which start after or before the "target" line @@ -21,10 +21,10 @@ respectively; see the -j option for more about the target line). The -a option causes forward searches to instead start at the bottom of the screen and backward searches to start at the top of the screen, thus skipping all lines displayed on the screen`, - }, - { - name: ["-A", "--SEARCH-SKIP-SCREEN"], - description: `Causes all forward searches (not just non-repeated searches) to + }, + { + name: ["-A", "--SEARCH-SKIP-SCREEN"], + description: `Causes all forward searches (not just non-repeated searches) to start just after the target line, and all backward searches to start just before the target line. Thus, forward searches will skip part of the displayed screen (from the first line up to and @@ -32,23 +32,23 @@ including the target line). Similarly backwards searches will skip the displayed screen from the last line up to and including the target line. This was the default behavior in less versions prior to 441`, - }, + }, - { - name: ["-b", "--buffers"], - args: { name: "n" }, - description: `Specifies the amount of buffer space less will use for each + { + name: ["-b", "--buffers"], + args: { name: "n" }, + description: `Specifies the amount of buffer space less will use for each file, in units of kilobytes (1024 bytes). By default 64 KB of buffer space is used for each file (unless the file is a pipe; see the -B option). The -b option specifies instead that n kilobytes of buffer space should be used for each file. If n is -1, buffer space is unlimited; that is, the entire file can be read into memory`, - }, + }, - { - name: ["-B", "--auto-buffers"], - description: `By default, when data is read from a pipe, buffers are allocated + { + name: ["-B", "--auto-buffers"], + description: `By default, when data is read from a pipe, buffers are allocated automatically as needed. If a large amount of data is read from the pipe, this can cause a large amount of memory to be allocated. The -B option disables this automatic allocation of @@ -57,33 +57,33 @@ specified by the -b option) is used for the pipe. Warning: use of -B can result in erroneous display, since only the most recently viewed part of the piped data is kept in memory; any earlier data is lost`, - }, + }, - { - name: ["-c", "--clear-screen"], - description: `Causes full screen repaints to be painted from the top line + { + name: ["-c", "--clear-screen"], + description: `Causes full screen repaints to be painted from the top line down. By default, full screen repaints are done by scrolling from the bottom of the screen`, - }, + }, - { - name: ["-C", "--CLEAR-SCREEN"], - description: `Same as -c, for compatibility with older versions of less`, - }, + { + name: ["-C", "--CLEAR-SCREEN"], + description: `Same as -c, for compatibility with older versions of less`, + }, - { - name: ["-d", "--dumb"], - description: `The -d option suppresses the error message normally displayed if + { + name: ["-d", "--dumb"], + description: `The -d option suppresses the error message normally displayed if the terminal is dumb; that is, lacks some important capability, such as the ability to clear the screen or scroll backward. The -d option does not otherwise change the behavior of less on a dumb terminal`, - }, + }, - { - name: ["-D", "--color"], - args: { name: "xcolor" }, - description: `Changes the color of different parts of the displayed text. x + { + name: ["-D", "--color"], + args: { name: "xcolor" }, + description: `Changes the color of different parts of the displayed text. x is a single character which selects the type of text whose color is being set: B Binary characters. @@ -144,80 +144,80 @@ color is set to that of normal text. On MS-DOS versions of less, 8-bit color is not supported; instead, decimal values are interpreted as 4-bit CHAR_INFO.Attributes values (see https://docs.microsoft.com/en-us/windows/console/char-info-str)`, - }, + }, - { - name: ["-e", "--quit-at-eof"], - description: `Causes less to automatically exit the second time it reaches + { + name: ["-e", "--quit-at-eof"], + description: `Causes less to automatically exit the second time it reaches end-of-file. By default, the only way to exit less is via the "q" command`, - }, + }, - { - name: ["-E", "--QUIT-AT-EOF"], - description: `Causes less to automatically exit the first time it reaches end- + { + name: ["-E", "--QUIT-AT-EOF"], + description: `Causes less to automatically exit the first time it reaches end- of-file`, - }, + }, - { - name: ["-f", "--force"], - description: `Forces non-regular files to be opened. (A non-regular file is a + { + name: ["-f", "--force"], + description: `Forces non-regular files to be opened. (A non-regular file is a directory or a device special file.) Also suppresses the warning message when a binary file is opened. By default, less will refuse to open non-regular files. Note that some operating systems will not allow directories to be read, even if -f is set`, - }, + }, - { - name: ["-F", "--quit-if-one-screen"], - description: `Causes less to automatically exit if the entire file can be + { + name: ["-F", "--quit-if-one-screen"], + description: `Causes less to automatically exit if the entire file can be displayed on the first screen`, - }, + }, - { - name: ["-g", "--hilite-search"], - description: `Normally, less will highlight ALL strings which match the last + { + name: ["-g", "--hilite-search"], + description: `Normally, less will highlight ALL strings which match the last search command. The -g option changes this behavior to highlight only the particular string which was found by the last search command. This can cause less to run somewhat faster than the default`, - }, + }, - { - name: ["-G", "--HILITE-SEARCH"], - description: `The -G option suppresses all highlighting of strings found by + { + name: ["-G", "--HILITE-SEARCH"], + description: `The -G option suppresses all highlighting of strings found by search commands`, - }, + }, - { - name: ["-h", "--max-back-scroll"], - args: { name: "n" }, - description: `Specifies a maximum number of lines to scroll backward. If it + { + name: ["-h", "--max-back-scroll"], + args: { name: "n" }, + description: `Specifies a maximum number of lines to scroll backward. If it is necessary to scroll backward more than n lines, the screen is repainted in a forward direction instead. (If the terminal does not have the ability to scroll backward, -h0 is implied.)`, - }, + }, - { - name: ["-i", "--ignore-case"], - description: `Causes searches to ignore case; that is, uppercase and lowercase + { + name: ["-i", "--ignore-case"], + description: `Causes searches to ignore case; that is, uppercase and lowercase are considered identical. This option is ignored if any uppercase letters appear in the search pattern; in other words, if a pattern contains uppercase letters, then that search does not ignore case`, - }, + }, - { - name: ["-I", "--IGNORE-CASE"], - description: `Like -i, but searches ignore case even if the pattern contains + { + name: ["-I", "--IGNORE-CASE"], + description: `Like -i, but searches ignore case even if the pattern contains uppercase letters`, - }, + }, - { - name: ["-j", "--jump-target"], - args: { name: "n" }, - description: `Specifies a line on the screen where the "target" line is to be + { + name: ["-j", "--jump-target"], + args: { name: "n" }, + description: `Specifies a line on the screen where the "target" line is to be positioned. The target line is the line specified by any command to search for a pattern, jump to a line number, jump to a file percentage or jump to a tag. The screen line may be @@ -240,57 +240,57 @@ fourth line on the screen, so forward searches begin at the fifth line on the screen. However nonrepeated searches (invoked with "/" or "?") always begin at the start or end of the current screen respectively`, - }, + }, - { - name: ["-J", "--status-column"], - description: `Displays a status column at the left edge of the screen. The + { + name: ["-J", "--status-column"], + description: `Displays a status column at the left edge of the screen. The status column shows the lines that matched the current search, and any lines that are marked (via the m or M command)`, - }, + }, - { - name: ["-k", "--lesskey-file"], - args: { name: "filename", template: "filepaths" }, - description: `Causes less to open and interpret the named file as a lesskey(1) + { + name: ["-k", "--lesskey-file"], + args: { name: "filename", template: "filepaths" }, + description: `Causes less to open and interpret the named file as a lesskey(1) file. Multiple -k options may be specified. If the LESSKEY or LESSKEY_SYSTEM environment variable is set, or if a lesskey file is found in a standard place (see KEY BINDINGS), it is also used as a lesskey file`, - }, + }, - { - name: ["-K", "--quit-on-intr"], - description: `Causes less to exit immediately (with status 2) when an + { + name: ["-K", "--quit-on-intr"], + description: `Causes less to exit immediately (with status 2) when an interrupt character (usually ^C) is typed. Normally, an interrupt character causes less to stop whatever it is doing and return to its command prompt. Note that use of this option makes it impossible to return to the command prompt from the "F" command`, - }, + }, - { - name: ["-L", "--no-lessopen"], - description: `Ignore the LESSOPEN environment variable (see the INPUT + { + name: ["-L", "--no-lessopen"], + description: `Ignore the LESSOPEN environment variable (see the INPUT PREPROCESSOR section below). This option can be set from within less, but it will apply only to files opened subsequently, not to the file which is currently open`, - }, + }, - { - name: ["-m", "--long-prompt"], - description: `Causes less to prompt verbosely (like more), with the percent + { + name: ["-m", "--long-prompt"], + description: `Causes less to prompt verbosely (like more), with the percent into the file. By default, less prompts with a colon`, - }, + }, - { - name: ["-M", "--LONG-PROMPT"], - description: `Causes less to prompt even more verbosely than more`, - }, + { + name: ["-M", "--LONG-PROMPT"], + description: `Causes less to prompt even more verbosely than more`, + }, - { - name: ["-n", "--line-numbers"], - description: `Suppresses line numbers. The default (to use line numbers) may + { + name: ["-n", "--line-numbers"], + description: `Suppresses line numbers. The default (to use line numbers) may cause less to run more slowly in some cases, especially with a very large input file. Suppressing line numbers with the -n option will avoid this problem. Using line numbers means: the @@ -298,46 +298,46 @@ line number will be displayed in the verbose prompt and in the = command, and the v command will pass the current line number to the editor (see also the discussion of LESSEDIT in PROMPTS below)`, - }, + }, - { - name: ["-N", "--LINE-NUMBERS"], - description: `Causes a line number to be displayed at the beginning of each + { + name: ["-N", "--LINE-NUMBERS"], + description: `Causes a line number to be displayed at the beginning of each line in the display`, - }, + }, - { - name: ["-o", "--log-file"], - args: { name: "filename", template: "filepaths" }, - description: `Causes less to copy its input to the named file as it is being + { + name: ["-o", "--log-file"], + args: { name: "filename", template: "filepaths" }, + description: `Causes less to copy its input to the named file as it is being viewed. This applies only when the input file is a pipe, not an ordinary file. If the file already exists, less will ask for confirmation before overwriting it`, - }, + }, - { - name: ["-O", "--LOG-FILE"], - args: { name: "filename", template: "filepaths" }, - description: `The -O option is like -o, but it will overwrite an existing file + { + name: ["-O", "--LOG-FILE"], + args: { name: "filename", template: "filepaths" }, + description: `The -O option is like -o, but it will overwrite an existing file without asking for confirmation. If no log file has been specified, the -o and -O options can be used from within less to specify a log file. Without a file name, they will simply report the name of the log file. The "s" command is equivalent to specifying -o from within less`, - }, + }, - { - name: ["-p", "--pattern"], - args: { name: "pattern" }, - description: `The -p option on the command line is equivalent to specifying + { + name: ["-p", "--pattern"], + args: { name: "pattern" }, + description: `The -p option on the command line is equivalent to specifying +/pattern; that is, it tells less to start at the first occurrence of pattern in the file`, - }, + }, - { - name: ["-P", "--prompt"], - args: { name: "prompt" }, - description: `Provides a way to tailor the three prompt styles to your own + { + name: ["-P", "--prompt"], + args: { name: "prompt" }, + description: `Provides a way to tailor the three prompt styles to your own preference. This option would normally be put in the LESS environment variable, rather than being typed in with each less command. Such an option must either be the last option in the @@ -352,28 +352,28 @@ that string. F command). All prompt strings consist of a sequence of letters and special escape sequences. See the section on PROMPTS for more details`, - }, + }, - { - name: ["-q", "--quiet", "--silent"], - description: `Causes moderately "quiet" operation: the terminal bell is not + { + name: ["-q", "--quiet", "--silent"], + description: `Causes moderately "quiet" operation: the terminal bell is not rung if an attempt is made to scroll past the end of the file or before the beginning of the file. If the terminal has a "visual bell", it is used instead. The bell will be rung on certain other errors, such as typing an invalid character. The default is to ring the terminal bell in all such cases`, - }, + }, - { - name: ["-Q", "--QUIET", "--SILENT"], - description: `Causes totally "quiet" operation: the terminal bell is never + { + name: ["-Q", "--QUIET", "--SILENT"], + description: `Causes totally "quiet" operation: the terminal bell is never rung. If the terminal has a "visual bell", it is used in all cases where the terminal bell would have been rung`, - }, + }, - { - name: ["-r", "--raw-control-chars"], - description: `Causes "raw" control characters to be displayed. The default is + { + name: ["-r", "--raw-control-chars"], + description: `Causes "raw" control characters to be displayed. The default is to display control characters using the caret notation; for example, a control-A (octal 001) is displayed as "^A". Warning: when the -r option is used, less cannot keep track of the actual @@ -382,11 +382,11 @@ responds to each type of control character). Thus, various display problems may result, such as long lines being split in the wrong place. USE OF THE -r OPTION IS NOT RECOMMENDED`, - }, + }, - { - name: ["-R", "--RAW-CONTROL-CHARS"], - description: `Like -r, but only ANSI "color" escape sequences and OSC 8 + { + name: ["-R", "--RAW-CONTROL-CHARS"], + description: `Like -r, but only ANSI "color" escape sequences and OSC 8 hyperlink sequences are output in "raw" form. Unlike -r, the screen appearance is maintained correctly, provided that there are no escape sequences in the file other than these types of @@ -408,27 +408,27 @@ escape sequence. And you can make less think that characters other than the standard ones may appear between the ESC and the m by setting the environment variable LESSANSIMIDCHARS to the list of characters which can appear`, - }, + }, - { - name: ["-s", "--squeeze-blank-lines"], - description: `Causes consecutive blank lines to be squeezed into a single + { + name: ["-s", "--squeeze-blank-lines"], + description: `Causes consecutive blank lines to be squeezed into a single blank line. This is useful when viewing nroff output`, - }, + }, - { - name: ["-S", "--chop-long-lines"], - description: `Causes lines longer than the screen width to be chopped + { + name: ["-S", "--chop-long-lines"], + description: `Causes lines longer than the screen width to be chopped (truncated) rather than wrapped. That is, the portion of a long line that does not fit in the screen width is not displayed until you press RIGHT-ARROW. The default is to wrap long lines; that is, display the remainder on the next line`, - }, + }, - { - name: ["-t", "--tag"], - args: { name: "tag" }, - description: `The -t option, followed immediately by a TAG, will edit the file + { + name: ["-t", "--tag"], + args: { name: "tag" }, + description: `The -t option, followed immediately by a TAG, will edit the file containing that tag. For this to work, tag information must be available; for example, there may be a file in the current directory called "tags", which was previously built by ctags(1) @@ -439,24 +439,24 @@ the tag. (See http://www.gnu.org/software/global/global.html). The -t option may also be specified from within less (using the - command) as a way of examining a new file. The command ":t" is equivalent to specifying -t from within less`, - }, + }, - { - name: ["-T", "--tag-file"], - args: { name: "tagsfile" }, - description: `Specifies a tags file to be used instead of "tags"`, - }, + { + name: ["-T", "--tag-file"], + args: { name: "tagsfile" }, + description: `Specifies a tags file to be used instead of "tags"`, + }, - { - name: ["-u", "--underline-special"], - description: `Causes backspaces and carriage returns to be treated as + { + name: ["-u", "--underline-special"], + description: `Causes backspaces and carriage returns to be treated as printable characters; that is, they are sent to the terminal when they appear in the input`, - }, + }, - { - name: ["-U", "--UNDERLINE-SPECIAL"], - description: `Causes backspaces, tabs, carriage returns and "formatting + { + name: ["-U", "--UNDERLINE-SPECIAL"], + description: `Causes backspaces, tabs, carriage returns and "formatting characters" (as defined by Unicode) to be treated as control characters; that is, they are handled as specified by the -r option. @@ -473,16 +473,16 @@ specified by the -r option. Unicode formatting characters, such as the Byte Order Mark, are sent to the terminal. Text which is overstruck or underlined can be searched for if neither -u nor -U is in effect`, - }, + }, - { - name: ["-V", "--version"], - description: `Displays the version number of less`, - }, + { + name: ["-V", "--version"], + description: `Displays the version number of less`, + }, - { - name: ["-w", "--hilite-unread"], - description: `Temporarily highlights the first "new" line after a forward + { + name: ["-w", "--hilite-unread"], + description: `Temporarily highlights the first "new" line after a forward movement of a full page. The first "new" line is the line immediately following the line previously at the bottom of the screen. Also highlights the target line after a g or p command. @@ -490,47 +490,47 @@ The highlight is removed at the next command which causes movement. The entire line is highlighted, unless the -J option is in effect, in which case only the status column is highlighted`, - }, + }, - { - name: ["-W", "--HILITE-UNREAD"], - description: `Like -w, but temporarily highlights the first new line after any + { + name: ["-W", "--HILITE-UNREAD"], + description: `Like -w, but temporarily highlights the first new line after any forward movement command larger than one line`, - }, + }, - { - name: ["-x", "--tabs="], - args: { name: "n,..." }, - description: `Sets tab stops. If only one n is specified, tab stops are set + { + name: ["-x", "--tabs="], + args: { name: "n,..." }, + description: `Sets tab stops. If only one n is specified, tab stops are set at multiples of n. If multiple values separated by commas are specified, tab stops are set at those positions, and then continue with the same spacing as the last two. For example, -x9,17 will set tabs at positions 9, 17, 25, 33, etc. The default for n is 8`, - }, + }, - { - name: ["-X", "--no-init"], - description: `Disables sending the termcap initialization and deinitialization + { + name: ["-X", "--no-init"], + description: `Disables sending the termcap initialization and deinitialization strings to the terminal. This is sometimes desirable if the deinitialization string does something unnecessary, like clearing the screen`, - }, + }, - { - name: ["-y", "--max-forw-scroll"], - args: { name: "n" }, - description: `Specifies a maximum number of lines to scroll forward. If it is + { + name: ["-y", "--max-forw-scroll"], + args: { name: "n" }, + description: `Specifies a maximum number of lines to scroll forward. If it is necessary to scroll forward more than n lines, the screen is repainted instead. The -c or -C option may be used to repaint from the top of the screen if desired. By default, any forward movement causes scrolling`, - }, + }, - { - name: ["-z", "--window"], - args: { name: "n" }, - description: `Changes the default scrolling window size to n lines. The + { + name: ["-z", "--window"], + args: { name: "n" }, + description: `Changes the default scrolling window size to n lines. The default is one screenful. The z and w commands can also be used to change the window size. The "z" may be omitted for compatibility with some versions of more. If the number n is @@ -538,11 +538,11 @@ negative, it indicates n lines less than the current screen size. For example, if the screen is 24 lines, -z-4 sets the scrolling window to 20 lines. If the screen is resized to 40 lines, the scrolling window automatically changes to 36 lines`, - }, + }, - { - name: "--quotes", - description: `Changes the filename quoting character. This may be necessary + { + name: "--quotes", + description: `Changes the filename quoting character. This may be necessary if you are trying to name a file which contains both spaces and quote characters. Followed by a single character, this changes the quote character to that character. Filenames containing a @@ -554,18 +554,18 @@ by the open quote character and followed by the close quote character. Note that even after the quote characters are changed, this option remains -" (a dash followed by a double quote)`, - }, + }, - { - name: ["-~", "--tilde"], - description: `Normally lines after end of file are displayed as a single tilde + { + name: ["-~", "--tilde"], + description: `Normally lines after end of file are displayed as a single tilde (~). This option causes lines after end of file to be displayed as blank lines`, - }, + }, - { - name: ["-#", "--shift"], - description: `Specifies the default number of positions to scroll horizontally + { + name: ["-#", "--shift"], + description: `Specifies the default number of positions to scroll horizontally in the RIGHTARROW and LEFTARROW commands. If the number specified is zero, it sets the default number of positions to one half of the screen width. Alternately, the number may be @@ -576,11 +576,11 @@ specified as a fraction, the actual number of scroll positions is recalculated if the terminal window is resized, so that the actual scroll remains at the specified fraction of the screen width`, - }, + }, - { - name: "--follow-name", - description: `Normally, if the input file is renamed while an F command is + { + name: "--follow-name", + description: `Normally, if the input file is renamed while an F command is executing, less will continue to display the contents of the original file despite its name change. If --follow-name is specified, during an F command less will periodically attempt to @@ -588,89 +588,89 @@ reopen the file by name. If the reopen succeeds and the file is a different file from the original (which means that a new file has been created with the same name as the original (now renamed) file), less will display the contents of that new file`, - }, - { - name: "--incsearch", - description: `Subsequent search commands will be "incremental"; that is, less + }, + { + name: "--incsearch", + description: `Subsequent search commands will be "incremental"; that is, less will advance to the next line containing the search pattern as each character of the pattern is typed in`, - }, + }, - { - name: "--line-num-width", - description: `Sets the minimum width of the line number field when the -N + { + name: "--line-num-width", + description: `Sets the minimum width of the line number field when the -N option is in effect. The default is 7 characters`, - }, - { - name: "--mouse", - description: `Enables mouse input: scrolling the mouse wheel down moves + }, + { + name: "--mouse", + description: `Enables mouse input: scrolling the mouse wheel down moves forward in the file, scrolling the mouse wheel up moves backwards in the file, and clicking the mouse sets the "#" mark to the line where the mouse is clicked. The number of lines to scroll when the wheel is moved can be set by the --wheel-lines option. Mouse input works only on terminals which support X11 mouse reporting, and on the Windows version of less`, - }, - { - name: "--MOUSE", - description: `Like --mouse, except the direction scrolled on mouse wheel + }, + { + name: "--MOUSE", + description: `Like --mouse, except the direction scrolled on mouse wheel movement is reversed`, - }, - { - name: "--no-keypad", - description: `Disables sending the keypad initialization and deinitialization + }, + { + name: "--no-keypad", + description: `Disables sending the keypad initialization and deinitialization strings to the terminal. This is sometimes useful if the keypad strings make the numeric keypad behave in an undesirable manner`, - }, - { - name: "--no-histdups", - description: `This option changes the behavior so that if a search string or + }, + { + name: "--no-histdups", + description: `This option changes the behavior so that if a search string or file name is typed in, and the same string is already in the history list, the existing copy is removed from the history list before the new one is added. Thus, a given string will appear only once in the history list. Normally, a string may appear multiple times`, - }, - { - name: "--rscroll", - description: `This option changes the character used to mark truncated lines. + }, + { + name: "--rscroll", + description: `This option changes the character used to mark truncated lines. It may begin with a two-character attribute indicator like LESSBINFMT does. If there is no attribute indicator, standout is used. If set to "-", truncated lines are not marked`, - }, - { - name: "--save-marks", - description: `Save marks in the history file, so marks are retained across + }, + { + name: "--save-marks", + description: `Save marks in the history file, so marks are retained across different invocations of less`, - }, - { - name: "--status-col-width", - description: `Sets the width of the status column when the -J option is in + }, + { + name: "--status-col-width", + description: `Sets the width of the status column when the -J option is in effect. The default is 2 characters`, - }, - { - name: "--use-backslash", - description: `This option changes the interpretations of options which follow + }, + { + name: "--use-backslash", + description: `This option changes the interpretations of options which follow this one. After the --use-backslash option, any backslash in an option string is removed and the following character is taken literally. This allows a dollar sign to be included in option strings`, - }, - { - name: "--use-color", - description: `Enables the colored text in various places. The -D option can + }, + { + name: "--use-color", + description: `Enables the colored text in various places. The -D option can be used to change the colors. Colored text works only if the terminal supports ANSI color escape sequences (as defined in ECMA-48 SGR; see https://www.ecma-international.org/publications-and- standards/standards/ecma-48)`, - }, - { - name: "--wheel-lines", - args: { name: "n" }, - description: `Set the number of lines to scroll when the mouse wheel is rolled`, - }, - ], + }, + { + name: "--wheel-lines", + args: { name: "n" }, + description: `Set the number of lines to scroll when the mouse wheel is rolled`, + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/mkdir.ts b/extensions/terminal-suggest/src/completions/upstream/mkdir.ts index 90a6530c3bd..2f9ff425d83 100644 --- a/extensions/terminal-suggest/src/completions/upstream/mkdir.ts +++ b/extensions/terminal-suggest/src/completions/upstream/mkdir.ts @@ -1,37 +1,37 @@ const completionSpec: Fig.Spec = { - name: "mkdir", - description: "Make directories", - args: { - name: "directory name", - template: "folders", - suggestCurrentToken: true, - }, - options: [ - { - name: ["-m", "--mode"], - description: "Set file mode (as in chmod), not a=rwx - umask", - args: { name: "MODE" }, - }, - { - name: ["-p", "--parents"], - description: "No error if existing, make parent directories as needed", - }, - { - name: ["-v", "--verbose"], - description: "Print a message for each created directory", - }, - { - name: ["-Z", "--context"], - description: - "Set the SELinux security context of each created directory to CTX", - args: { name: "CTX" }, - }, - { name: "--help", description: "Display this help and exit" }, - { - name: "--version", - description: "Output version information and exit", - }, - ], + name: "mkdir", + description: "Make directories", + args: { + name: "directory name", + template: "folders", + suggestCurrentToken: true, + }, + options: [ + { + name: ["-m", "--mode"], + description: "Set file mode (as in chmod), not a=rwx - umask", + args: { name: "MODE" }, + }, + { + name: ["-p", "--parents"], + description: "No error if existing, make parent directories as needed", + }, + { + name: ["-v", "--verbose"], + description: "Print a message for each created directory", + }, + { + name: ["-Z", "--context"], + description: + "Set the SELinux security context of each created directory to CTX", + args: { name: "CTX" }, + }, + { name: "--help", description: "Display this help and exit" }, + { + name: "--version", + description: "Output version information and exit", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/more.ts b/extensions/terminal-suggest/src/completions/upstream/more.ts index a70c8ee9078..6a9d6e82066 100644 --- a/extensions/terminal-suggest/src/completions/upstream/more.ts +++ b/extensions/terminal-suggest/src/completions/upstream/more.ts @@ -1,55 +1,55 @@ const completionSpec: Fig.Spec = { - name: "more", - description: "Opposite of less", - options: [ - { - name: ["-d", "--silent"], - description: - "Prompt with '[Press space to continue, 'q' to quit.]', and display '[Press 'h' for instructions.]' instead of ringing the bell when an illegal key is pressed", - }, - { - name: ["-l", "--logical"], - description: "Do not pause after any line containing a ^L (form feed)", - }, - { - name: ["-f", "--no-pause"], - description: "Count logical lines, rather than screen lines", - }, - { - name: ["-p", "--print-over"], - description: "Instead, clear the whole screen and then display the text", - }, - { - name: ["-c", "--clean-print"], - description: - "Instead, paint each screen from the top, clearing the remainder of each line as it is displayed", - }, - { - name: ["-s", "--squeeze"], - description: "Squeeze multiple blank lines into one", - }, - { - name: ["-u", "--plain"], - description: "Silently ignored as backwards compatibility", - }, - { - name: ["-n", "--lines"], - description: "Specify the number of lines per screenful", - args: { name: "n" }, - }, - { - name: "--help", - description: "Display help text", - }, - { - name: ["-V", "--version"], - description: "Display version information", - }, - ], - args: { - isVariadic: true, - template: "filepaths", - }, + name: "more", + description: "Opposite of less", + options: [ + { + name: ["-d", "--silent"], + description: + "Prompt with '[Press space to continue, 'q' to quit.]', and display '[Press 'h' for instructions.]' instead of ringing the bell when an illegal key is pressed", + }, + { + name: ["-l", "--logical"], + description: "Do not pause after any line containing a ^L (form feed)", + }, + { + name: ["-f", "--no-pause"], + description: "Count logical lines, rather than screen lines", + }, + { + name: ["-p", "--print-over"], + description: "Instead, clear the whole screen and then display the text", + }, + { + name: ["-c", "--clean-print"], + description: + "Instead, paint each screen from the top, clearing the remainder of each line as it is displayed", + }, + { + name: ["-s", "--squeeze"], + description: "Squeeze multiple blank lines into one", + }, + { + name: ["-u", "--plain"], + description: "Silently ignored as backwards compatibility", + }, + { + name: ["-n", "--lines"], + description: "Specify the number of lines per screenful", + args: { name: "n" }, + }, + { + name: "--help", + description: "Display help text", + }, + { + name: ["-V", "--version"], + description: "Display version information", + }, + ], + args: { + isVariadic: true, + template: "filepaths", + }, }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/mv.ts b/extensions/terminal-suggest/src/completions/upstream/mv.ts index bbf98bf4903..2109b0dd0ff 100644 --- a/extensions/terminal-suggest/src/completions/upstream/mv.ts +++ b/extensions/terminal-suggest/src/completions/upstream/mv.ts @@ -1,40 +1,40 @@ const completionSpec: Fig.Spec = { - name: "mv", - description: "Move & rename files and folders", - args: [ - { - name: "source", - isVariadic: true, - template: ["filepaths", "folders"], - }, - { - name: "target", - template: ["filepaths", "folders"], - }, - ], - options: [ - { - name: "-f", - description: - "Do not prompt for confirmation before overwriting the destination path", - exclusiveOn: ["-i", "-n"], - }, - { - name: "-i", - description: - "Cause mv to write a prompt to standard error before moving a file that would overwrite an existing file", - exclusiveOn: ["-f", "-n"], - }, - { - name: "-n", - description: "Do not overwrite existing file", - exclusiveOn: ["-f", "-i"], - }, - { - name: "-v", - description: "Cause mv to be verbose, showing files after they are moved", - }, - ], + name: "mv", + description: "Move & rename files and folders", + args: [ + { + name: "source", + isVariadic: true, + template: ["filepaths", "folders"], + }, + { + name: "target", + template: ["filepaths", "folders"], + }, + ], + options: [ + { + name: "-f", + description: + "Do not prompt for confirmation before overwriting the destination path", + exclusiveOn: ["-i", "-n"], + }, + { + name: "-i", + description: + "Cause mv to write a prompt to standard error before moving a file that would overwrite an existing file", + exclusiveOn: ["-f", "-n"], + }, + { + name: "-n", + description: "Do not overwrite existing file", + exclusiveOn: ["-f", "-i"], + }, + { + name: "-v", + description: "Cause mv to be verbose, showing files after they are moved", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/nano.ts b/extensions/terminal-suggest/src/completions/upstream/nano.ts index 6064072e873..caa84c34d65 100644 --- a/extensions/terminal-suggest/src/completions/upstream/nano.ts +++ b/extensions/terminal-suggest/src/completions/upstream/nano.ts @@ -1,9 +1,9 @@ const completionSpec: Fig.Spec = { - name: "nano", - description: "Nano's ANOther editor, an enhanced free Pico clone", - args: { - template: "filepaths", - }, + name: "nano", + description: "Nano's ANOther editor, an enhanced free Pico clone", + args: { + template: "filepaths", + }, }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/node.ts b/extensions/terminal-suggest/src/completions/upstream/node.ts index b967b854573..d6e7b52e17d 100644 --- a/extensions/terminal-suggest/src/completions/upstream/node.ts +++ b/extensions/terminal-suggest/src/completions/upstream/node.ts @@ -82,423 +82,422 @@ const completionSpec: Fig.Subcommand = { "Follows symlinks to directories when examining source code and templates for translation strings", }, ], - // generateSpec: async (tokens, executeShellCommand) => { - // const isAdonisJsonPresentCommand = "test -f .adonisrc.json"; - // if ( - // ( - // await executeShellCommand({ - // command: "bash", - // args: ["-c", "isAdonisJsonPresentCommand"], - // }) - // ).status === 0 - // ) { - // return { - // name: "node", - // subcommands: [ - // { - // name: "ace", - // description: "Run AdonisJS command-line", - // options: [ - // { - // name: ["-h", "--help"], - // description: "Display AdonisJS Ace help", - // }, - // { - // name: ["-v", "--version"], - // description: "Display AdonisJS version", - // }, - // ], - // subcommands: [ - // { - // name: "build", - // description: - // "Compile project from Typescript to Javascript. Also compiles the frontend assets if using webpack encore", - // options: [ - // { - // name: ["-prod", "--production"], - // description: "Build for production", - // }, - // { - // name: "--assets", - // description: - // "Build frontend assets when webpack encore is installed", - // }, - // { - // name: "--no-assets", - // description: "Disable building assets", - // }, - // { - // name: "--ignore-ts-errors", - // description: - // "Ignore typescript errors and complete the build process", - // }, - // { - // name: "--tsconfig", - // description: - // "Path to the TypeScript project configuration file", - // args: { - // name: "path", - // description: "Path to tsconfig.json", - // }, - // }, - // { - // name: "--encore-args", - // requiresSeparator: true, - // insertValue: "--encore-args='{cursor}'", - // description: - // "CLI options to pass to the encore command line", - // }, - // { - // name: "--client", - // args: { - // name: "name", - // }, - // description: - // "Select the package manager to decide which lock file to copy to the build folder", - // }, - // ], - // }, - // { - // name: ["configure", "invoke"], - // description: "Configure a given AdonisJS package", - // args: { - // name: "name", - // description: "Name of the package you want to configure", - // }, - // subcommands: [ - // { - // name: "@adonisjs/auth", - // description: "Trigger auto configuring auth package", - // }, - // { - // name: "@adonisjs/shield", - // description: "Trigger auto configuring shield package", - // }, - // { - // name: "@adonisjs/redis", - // description: "Trigger auto configuring redis package", - // }, - // { - // name: "@adonisjs/mail", - // description: "Trigger auto configuring mail package", - // }, - // ], - // }, - // { - // name: "repl", - // description: "Start a new REPL session", - // }, - // { - // name: "serve", - // description: - // "Start the AdonisJS HTTP server, along with the file watcher. Also starts the webpack dev server when webpack encore is installed", - // options: [ - // { - // name: "--assets", - // description: - // "Start webpack dev server when encore is installed", - // }, - // { - // name: "--no-assets", - // description: "Disable webpack dev server", - // }, - // { - // name: ["-w", "--watch"], - // description: - // "Watch for file changes and re-start the HTTP server on change", - // }, - // { - // name: ["-p", "--poll"], - // description: - // "Detect file changes by polling files instead of listening to filesystem events", - // }, - // { - // name: "--node-args", - // requiresSeparator: true, - // insertValue: "--node-args='{cursor}'", - // description: "CLI options to pass to the node command line", - // }, - // { - // name: "--encore-args", - // requiresSeparator: true, - // insertValue: "--encore-args='{cursor}'", - // description: - // "CLI options to pass to the encore command line", - // }, - // ], - // }, - // { - // name: "db:seed", - // description: "Execute database seeder files", - // options: [ - // { - // name: ["-c", "--connection"], - // description: - // "Define a custom database connection for the seeders", - // args: { - // name: "name", - // }, - // }, - // { - // name: ["-i", "--interactive"], - // description: "Run seeders in interactive mode", - // }, - // { - // name: ["-f", "--files"], - // args: { - // name: "file", - // isVariadic: true, - // template: "filepaths", - // }, - // description: - // "Define a custom set of seeders files names to run", - // }, - // ], - // }, - // { - // name: "dump:rcfile", - // description: - // "Dump contents of .adonisrc.json file along with defaults", - // }, - // { - // name: "generate:key", - // description: "Generate a new APP_KEY secret", - // }, - // { - // name: "generate:manifest", - // description: - // "Generate ace commands manifest file. Manifest file speeds up commands lookup", - // }, - // { - // name: "list:routes", - // description: "List application routes", - // }, - // { - // name: "make:command", - // description: "Make a new ace command", - // }, - // { - // name: "make:controller", - // description: "Make a new HTTP controller", - // args: { - // name: "name", - // description: "Name of the controller class", - // }, - // options: [ - // { - // name: ["-r", "--resource"], - // description: - // "Add resourceful methods to the controller class", - // }, - // { - // name: ["-e", "--exact"], - // description: - // "Create the controller with the exact name as provided", - // }, - // ], - // }, - // { - // name: "make:exception", - // description: "Make a new custom exception class", - // }, - // { - // name: "make:listener", - // description: "Make a new event listener class", - // }, - // { - // name: "make:mailer", - // description: "Make a new mailer class", - // args: { - // name: "name", - // description: "Mailer class name", - // }, - // }, - // { - // name: "make:middleware", - // description: "Make a new middleware", - // args: { - // name: "name", - // description: "Middleware class name", - // }, - // }, - // { - // name: "make:migration", - // description: "Make a new migration file", - // args: { - // name: "name", - // description: "Name of the migration file", - // }, - // options: [ - // { - // name: "--connection", - // description: - // "The connection flag is used to lookup the directory for the migration file", - // args: { - // name: "name", - // }, - // }, - // { - // name: "--folder", - // description: "Pre-select a migration directory", - // args: { - // name: "name", - // template: "filepaths", - // }, - // }, - // { - // name: "--create", - // description: - // "Define the table name for creating a new table", - // args: { - // name: "name", - // }, - // }, - // { - // name: "--table", - // description: - // "Define the table name for altering an existing table", - // args: { - // name: "name", - // }, - // }, - // ], - // }, - // { - // name: "make:model", - // description: "Make a new Lucid model", - // args: { - // name: "name", - // description: "Name of the model class", - // }, - // options: [ - // { - // name: ["-m", "--migration"], - // description: "Generate the migration for the model", - // }, - // { - // name: ["-c", "--controller"], - // description: "Generate the controller for the model", - // }, - // ], - // }, - // { - // name: "make:prldfile", - // description: "Make a new preload file", - // subcommands: [ - // { - // name: "events", - // description: "Make events preload file", - // }, - // ], - // }, - // { - // name: "make:provider", - // description: "Make a new provider class", - // }, - // { - // name: "make:seeder", - // description: "Make a new Seeder file", - // args: { - // name: "name", - // description: "Name of the seeder class", - // }, - // }, - // { - // name: "make:validator", - // description: "Make a new validator", - // args: { - // name: "name", - // description: "Name of the validator class", - // }, - // options: [ - // { - // name: ["-e", "--exact"], - // description: - // "Create the validator with the exact name as provided", - // }, - // ], - // }, - // { - // name: "make:view", - // description: "Make a new view template", - // args: { - // name: "name", - // description: "Name of the view", - // }, - // options: [ - // { - // name: ["-e", "--exact"], - // description: - // "Create the template file with the exact name as provided", - // }, - // ], - // }, - // { - // name: "migration:rollback", - // description: "Rollback migrations to a given batch number", - // options: [ - // { - // name: ["-c", "--connection"], - // description: "Define a custom database connection", - // args: { - // name: "name", - // }, - // }, - // { - // name: "--force", - // description: - // "Explicitly force to run migrations in production", - // isDangerous: true, - // }, - // { - // name: "--dry-run", - // description: - // "Print SQL queries, instead of running the migrations", - // }, - // { - // name: "--batch", - // args: { - // name: "number", - // description: "Use 0 to rollback to initial state", - // }, - // description: "Define custom batch number for rollback", - // }, - // ], - // }, - // { - // name: "migration:run", - // description: "Run pending migrations", - // options: [ - // { - // name: ["-c", "--connection"], - // description: "Define a custom database connection", - // args: { - // name: "name", - // }, - // }, - // { - // name: "--force", - // description: - // "Explicitly force to run migrations in production", - // isDangerous: true, - // }, - // { - // name: "--dry-run", - // description: - // "Print SQL queries, instead of running the migrations", - // }, - // ], - // }, - // { - // name: "migration:status", - // description: "Check migrations current status", - // }, - // ], - // }, - // ], - // }; - // } - // }, + generateSpec: async (tokens, executeShellCommand) => { + if ( + ( + await executeShellCommand({ + command: "bash", + args: ["-c", "isAdonisJsonPresentCommand"], + }) + ).status === 0 + ) { + return { + name: "node", + subcommands: [ + { + name: "ace", + description: "Run AdonisJS command-line", + options: [ + { + name: ["-h", "--help"], + description: "Display AdonisJS Ace help", + }, + { + name: ["-v", "--version"], + description: "Display AdonisJS version", + }, + ], + subcommands: [ + { + name: "build", + description: + "Compile project from Typescript to Javascript. Also compiles the frontend assets if using webpack encore", + options: [ + { + name: ["-prod", "--production"], + description: "Build for production", + }, + { + name: "--assets", + description: + "Build frontend assets when webpack encore is installed", + }, + { + name: "--no-assets", + description: "Disable building assets", + }, + { + name: "--ignore-ts-errors", + description: + "Ignore typescript errors and complete the build process", + }, + { + name: "--tsconfig", + description: + "Path to the TypeScript project configuration file", + args: { + name: "path", + description: "Path to tsconfig.json", + }, + }, + { + name: "--encore-args", + requiresSeparator: true, + insertValue: "--encore-args='{cursor}'", + description: + "CLI options to pass to the encore command line", + }, + { + name: "--client", + args: { + name: "name", + }, + description: + "Select the package manager to decide which lock file to copy to the build folder", + }, + ], + }, + { + name: ["configure", "invoke"], + description: "Configure a given AdonisJS package", + args: { + name: "name", + description: "Name of the package you want to configure", + }, + subcommands: [ + { + name: "@adonisjs/auth", + description: "Trigger auto configuring auth package", + }, + { + name: "@adonisjs/shield", + description: "Trigger auto configuring shield package", + }, + { + name: "@adonisjs/redis", + description: "Trigger auto configuring redis package", + }, + { + name: "@adonisjs/mail", + description: "Trigger auto configuring mail package", + }, + ], + }, + { + name: "repl", + description: "Start a new REPL session", + }, + { + name: "serve", + description: + "Start the AdonisJS HTTP server, along with the file watcher. Also starts the webpack dev server when webpack encore is installed", + options: [ + { + name: "--assets", + description: + "Start webpack dev server when encore is installed", + }, + { + name: "--no-assets", + description: "Disable webpack dev server", + }, + { + name: ["-w", "--watch"], + description: + "Watch for file changes and re-start the HTTP server on change", + }, + { + name: ["-p", "--poll"], + description: + "Detect file changes by polling files instead of listening to filesystem events", + }, + { + name: "--node-args", + requiresSeparator: true, + insertValue: "--node-args='{cursor}'", + description: "CLI options to pass to the node command line", + }, + { + name: "--encore-args", + requiresSeparator: true, + insertValue: "--encore-args='{cursor}'", + description: + "CLI options to pass to the encore command line", + }, + ], + }, + { + name: "db:seed", + description: "Execute database seeder files", + options: [ + { + name: ["-c", "--connection"], + description: + "Define a custom database connection for the seeders", + args: { + name: "name", + }, + }, + { + name: ["-i", "--interactive"], + description: "Run seeders in interactive mode", + }, + { + name: ["-f", "--files"], + args: { + name: "file", + isVariadic: true, + template: "filepaths", + }, + description: + "Define a custom set of seeders files names to run", + }, + ], + }, + { + name: "dump:rcfile", + description: + "Dump contents of .adonisrc.json file along with defaults", + }, + { + name: "generate:key", + description: "Generate a new APP_KEY secret", + }, + { + name: "generate:manifest", + description: + "Generate ace commands manifest file. Manifest file speeds up commands lookup", + }, + { + name: "list:routes", + description: "List application routes", + }, + { + name: "make:command", + description: "Make a new ace command", + }, + { + name: "make:controller", + description: "Make a new HTTP controller", + args: { + name: "name", + description: "Name of the controller class", + }, + options: [ + { + name: ["-r", "--resource"], + description: + "Add resourceful methods to the controller class", + }, + { + name: ["-e", "--exact"], + description: + "Create the controller with the exact name as provided", + }, + ], + }, + { + name: "make:exception", + description: "Make a new custom exception class", + }, + { + name: "make:listener", + description: "Make a new event listener class", + }, + { + name: "make:mailer", + description: "Make a new mailer class", + args: { + name: "name", + description: "Mailer class name", + }, + }, + { + name: "make:middleware", + description: "Make a new middleware", + args: { + name: "name", + description: "Middleware class name", + }, + }, + { + name: "make:migration", + description: "Make a new migration file", + args: { + name: "name", + description: "Name of the migration file", + }, + options: [ + { + name: "--connection", + description: + "The connection flag is used to lookup the directory for the migration file", + args: { + name: "name", + }, + }, + { + name: "--folder", + description: "Pre-select a migration directory", + args: { + name: "name", + template: "filepaths", + }, + }, + { + name: "--create", + description: + "Define the table name for creating a new table", + args: { + name: "name", + }, + }, + { + name: "--table", + description: + "Define the table name for altering an existing table", + args: { + name: "name", + }, + }, + ], + }, + { + name: "make:model", + description: "Make a new Lucid model", + args: { + name: "name", + description: "Name of the model class", + }, + options: [ + { + name: ["-m", "--migration"], + description: "Generate the migration for the model", + }, + { + name: ["-c", "--controller"], + description: "Generate the controller for the model", + }, + ], + }, + { + name: "make:prldfile", + description: "Make a new preload file", + subcommands: [ + { + name: "events", + description: "Make events preload file", + }, + ], + }, + { + name: "make:provider", + description: "Make a new provider class", + }, + { + name: "make:seeder", + description: "Make a new Seeder file", + args: { + name: "name", + description: "Name of the seeder class", + }, + }, + { + name: "make:validator", + description: "Make a new validator", + args: { + name: "name", + description: "Name of the validator class", + }, + options: [ + { + name: ["-e", "--exact"], + description: + "Create the validator with the exact name as provided", + }, + ], + }, + { + name: "make:view", + description: "Make a new view template", + args: { + name: "name", + description: "Name of the view", + }, + options: [ + { + name: ["-e", "--exact"], + description: + "Create the template file with the exact name as provided", + }, + ], + }, + { + name: "migration:rollback", + description: "Rollback migrations to a given batch number", + options: [ + { + name: ["-c", "--connection"], + description: "Define a custom database connection", + args: { + name: "name", + }, + }, + { + name: "--force", + description: + "Explicitly force to run migrations in production", + isDangerous: true, + }, + { + name: "--dry-run", + description: + "Print SQL queries, instead of running the migrations", + }, + { + name: "--batch", + args: { + name: "number", + description: "Use 0 to rollback to initial state", + }, + description: "Define custom batch number for rollback", + }, + ], + }, + { + name: "migration:run", + description: "Run pending migrations", + options: [ + { + name: ["-c", "--connection"], + description: "Define a custom database connection", + args: { + name: "name", + }, + }, + { + name: "--force", + description: + "Explicitly force to run migrations in production", + isDangerous: true, + }, + { + name: "--dry-run", + description: + "Print SQL queries, instead of running the migrations", + }, + ], + }, + { + name: "migration:status", + description: "Check migrations current status", + }, + ], + }, + ], + }; + } + }, }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/npm.ts b/extensions/terminal-suggest/src/completions/upstream/npm.ts index b48d8126444..aa142e05661 100644 --- a/extensions/terminal-suggest/src/completions/upstream/npm.ts +++ b/extensions/terminal-suggest/src/completions/upstream/npm.ts @@ -16,80 +16,82 @@ const atsInStr = (s: string) => (s.match(/@/g) || []).length; export const createNpmSearchHandler = (keywords?: string[]) => - async ( - context: string[], - executeShellCommand: Fig.ExecuteCommandFunction, - shellContext: Fig.ShellContext - ): Promise => { - const searchTerm = context[context.length - 1]; - if (searchTerm === "") { - return []; + async ( + context: string[], + executeShellCommand: Fig.ExecuteCommandFunction, + shellContext: Fig.ShellContext + ): Promise => { + const searchTerm = context[context.length - 1]; + if (searchTerm === "") { + return []; + } + // Add optional keyword parameter + const keywordParameter = + keywords && keywords.length > 0 ? `+keywords:${keywords.join(",")}` : ""; + + const queryPackagesUrl = keywordParameter + ? `https://api.npms.io/v2/search?size=20&q=${searchTerm}${keywordParameter}` + : `https://api.npms.io/v2/search/suggestions?q=${searchTerm}&size=20`; + + // Query the API with the package name + const queryPackages = [ + "-s", + "-H", + "Accept: application/json", + queryPackagesUrl, + ]; + // We need to remove the '@' at the end of the searchTerm before querying versions + const queryVersions = [ + "-s", + "-H", + "Accept: application/vnd.npm.install-v1+json", + `https://registry.npmjs.org/${searchTerm.slice(0, -1)}`, + ]; + // If the end of our token is '@', then we want to generate version suggestions + // Otherwise, we want packages + const out = (query: string) => + executeShellCommand({ + command: "curl", + args: query[query.length - 1] === "@" ? queryVersions : queryPackages, + }); + // If our token starts with '@', then a 2nd '@' tells us we want + // versions. + // Otherwise, '@' anywhere else in the string will indicate the same. + const shouldGetVersion = searchTerm.startsWith("@") + ? atsInStr(searchTerm) > 1 + : searchTerm.includes("@"); + + try { + const data = JSON.parse((await out(searchTerm)).stdout); + if (shouldGetVersion) { + // create dist tags suggestions + const versions = Object.entries(data["dist-tags"] || {}).map( + ([key, value]) => ({ + name: key, + description: value, + }) + ) as Fig.Suggestion[]; + // create versions + versions.push( + ...Object.keys(data.versions) + .map((version) => ({ name: version }) as Fig.Suggestion) + .reverse() + ); + return versions; } - // Add optional keyword parameter - const keywordParameter = - keywords && keywords.length > 0 ? `+keywords:${keywords.join(",")}` : ""; - const queryPackagesUrl = keywordParameter - ? `https://api.npms.io/v2/search?size=20&q=${searchTerm}${keywordParameter}` - : `https://api.npms.io/v2/search/suggestions?q=${searchTerm}&size=20`; - - // Query the API with the package name - const queryPackages = [ - "-s", - "-H", - "Accept: application/json", - queryPackagesUrl, - ]; - // We need to remove the '@' at the end of the searchTerm before querying versions - const queryVersions = [ - "-s", - "-H", - "Accept: application/vnd.npm.install-v1+json", - `https://registry.npmjs.org/${searchTerm.slice(0, -1)}`, - ]; - // If the end of our token is '@', then we want to generate version suggestions - // Otherwise, we want packages - const out = (query: string) => - executeShellCommand({ - command: "curl", - args: query[query.length - 1] === "@" ? queryVersions : queryPackages, - }); - // If our token starts with '@', then a 2nd '@' tells us we want - // versions. - // Otherwise, '@' anywhere else in the string will indicate the same. - const shouldGetVersion = searchTerm.startsWith("@") - ? atsInStr(searchTerm) > 1 - : searchTerm.includes("@"); - - try { - const data = JSON.parse((await out(searchTerm)).stdout); - if (shouldGetVersion) { - // create dist tags suggestions - const versions = Object.entries(data["dist-tags"] || {}).map( - ([key, value]) => ({ - name: key, - description: value, - }) - ) as Fig.Suggestion[]; - // create versions - versions.push( - ...Object.keys(data.versions) - .map((version) => ({ name: version }) as Fig.Suggestion) - .reverse() - ); - return versions; - } - - const results = keywordParameter ? data.results : data; - return results.map((item: any) => ({ + const results = keywordParameter ? data.results : data; + return results.map( + (item: { package: { name: string; description: string } }) => ({ name: item.package.name, description: item.package.description, - })) as Fig.Suggestion[]; - } catch (error) { - console.error({ error }); - return []; - } - }; + }) + ) as Fig.Suggestion[]; + } catch (error) { + console.error({ error }); + return []; + } + }; // GENERATORS export const npmSearchGenerator: Fig.Generator = { diff --git a/extensions/terminal-suggest/src/completions/upstream/npx.ts b/extensions/terminal-suggest/src/completions/upstream/npx.ts deleted file mode 100644 index 4143ed790d1..00000000000 --- a/extensions/terminal-suggest/src/completions/upstream/npx.ts +++ /dev/null @@ -1,317 +0,0 @@ -// import autocannon from "./autocannon"; - -export const npxSuggestions: Fig.Suggestion[] = [ - // { - // name: autocannon.name, - // ...("icon" in autocannon && { icon: autocannon.icon }), - // }, - { - name: "vite", - icon: "https://vitejs.dev/logo.svg", - }, - { - name: "babel", - icon: "https://raw.githubusercontent.com/babel/logo/master/babel.png", - }, - { - name: "create-react-native-app", - icon: "https://reactnative.dev/img/pwa/manifest-icon-512.png", - }, - { - name: "react-native", - icon: "https://reactnative.dev/img/pwa/manifest-icon-512.png", - }, - { - name: "tailwindcss", - icon: "https://tailwindcss.com/favicons/favicon-32x32.png", - }, - { - name: "next", - icon: "https://nextjs.org/static/favicon/favicon-16x16.png", - }, - { - name: "nuxi", - icon: "https://raw.githubusercontent.com/nuxt/framework/main/docs/public/icon.png", - }, - { - name: "gltfjsx", - icon: "https://raw.githubusercontent.com/pmndrs/branding/master/logo.svg", - }, - { - name: "prisma", - icon: "https://raw.githubusercontent.com/prisma/docs/main/src/images/favicon-16x16.png", - }, - { - name: "eslint", - icon: "https://raw.githubusercontent.com/eslint/eslint.org/main/src/static/icon-512.png", - }, - { - name: "prettier", - icon: "https://prettier.io/icon.png", - }, - { - name: "tsc", - icon: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Typescript_logo_2020.svg/240px-Typescript_logo_2020.svg.png", - }, - { - name: "typeorm", - icon: "https://avatars.githubusercontent.com/u/20165699?s=200&v=4", - }, - // { - // name: "fig-teams", - // icon: "https://fig.io/icons/fig-light.png", - // }, - { - name: "@withfig/autocomplete-tools", - icon: "https://fig.io/icons/fig-light.png", - }, - { - name: "create-completion-spec", - icon: "https://fig.io/icons/fig-light.png", - }, - { - name: "@fig/publish-spec-to-team", - icon: "https://fig.io/icons/fig-light.png", - }, - { - name: "fig-teams@latest", - icon: "https://fig.io/icons/fig-light.png", - }, - { - name: "create-next-app", - icon: "https://nextjs.org/static/favicon/favicon-16x16.png", - }, - { - name: "create-t3-app", - icon: "https://create.t3.gg/favicon.svg", - }, - { - name: "create-discord-bot", - icon: "https://discordjs.dev/favicon-32x32.png", - }, - { - name: "create-video", - icon: "https://raw.githubusercontent.com/remotion-dev/remotion/main/packages/docs/static/img/logo-small.png", - }, - { - name: "remotion", - icon: "https://raw.githubusercontent.com/remotion-dev/remotion/main/packages/docs/static/img/logo-small.png", - }, - { - name: "create-remix", - icon: "https://remix.run/favicon-light.1.png", - }, - { - name: "remix", - icon: "https://remix.run/favicon-light.1.png", - }, - { - name: "playwright", - icon: "https://playwright.dev/img/playwright-logo.svg", - }, - { - name: "ignite-cli", - icon: "🔥", - }, - { - name: "vsce", - }, - { - name: "degit", - icon: "fig://icon?type=git", - }, - { - name: "@preset/cli", - icon: "https://raw.githubusercontent.com/preset/preset/main/.github/assets/logo.svg", - }, - { - name: "mikro-orm", - icon: "https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/favicon.ico", - }, - { - name: "pod-install", - }, - { - name: "capacitor", - icon: "https://capacitorjs.com/docs/img/meta/favicon.png", - }, - { - name: "cap", - icon: "https://capacitorjs.com/docs/img/meta/favicon.png", - }, - { - name: "@magnolia/cli", - icon: "https://avatars.githubusercontent.com/u/25686615?s=200&v=4", - }, - { - name: "stencil", - icon: "https://stenciljs.com/assets/icon/favicon.ico", - }, - { - name: "swagger-typescript-api", - icon: "https://static1.smartbear.co/swagger/media/assets/swagger_fav.png", - }, - { - name: "sta", - icon: "https://static1.smartbear.co/swagger/media/assets/swagger_fav.png", - }, - { - name: "@wordpress/create-block", - icon: "https://s1.wp.com/i/webclip.png", - }, - { - name: "astro", - icon: "https://astro.build/favicon.svg", - }, - { - name: "ampx", - icon: "https://raw.githubusercontent.com/aws-amplify/docs/refs/heads/main/public/favicon.ico", - }, -]; - -const completionSpec: Fig.Spec = { - name: "npx", - description: "Execute binaries from npm packages", - args: { - name: "command", - isCommand: true, - generators: { - script: [ - "bash", - "-c", - "until [[ -d node_modules/ ]] || [[ $PWD = '/' ]]; do cd ..; done; ls -1 node_modules/.bin/", - ], - postProcess: function (out) { - const cli = [...npxSuggestions].reduce( - (acc: any, { name }) => [...acc, name], - [] - ); - return out - .split("\n") - .filter((name) => !cli.includes(name)) - .map((name) => ({ - name, - icon: "fig://icon?type=command", - loadSpec: name, - })); - }, - }, - suggestions: [...npxSuggestions], - isOptional: true, - }, - - options: [ - { - name: ["--package", "-p"], - description: "Package to be installed", - args: { - name: "package", - }, - }, - { - name: "--cache", - args: { - name: "path", - template: "filepaths", - }, - description: "Location of the npm cache", - }, - { - name: "--always-spawn", - description: "Always spawn a child process to execute the command", - }, - { - name: "-y", - description: "Execute npx command without prompting for confirmation", - }, - { - description: "Skip installation if a package is missing", - name: "--no-install", - }, - { - args: { - name: "path", - template: "filepaths", - }, - description: "Path to user npmrc", - name: "--userconfig", - }, - { - name: ["--call", "-c"], - args: { - name: "script", - }, - description: "Execute string as if inside `npm run-script`", - }, - { - name: ["--shell", "-s"], - description: "Shell to execute the command with, if any", - args: { - name: "shell", - suggestions: [ - { - name: "bash", - }, - { - name: "fish", - }, - { - name: "zsh", - }, - ], - }, - }, - { - args: { - name: "shell-fallback", - suggestions: [ - { - name: "bash", - }, - { - name: "fish", - }, - { - name: "zsh", - }, - ], - }, - name: "--shell-auto-fallback", - description: - 'Generate shell code to use npx as the "command not found" fallback', - }, - { - name: "--ignore-existing", - description: - "Ignores existing binaries in $PATH, or in the localproject. This forces npx to do a temporary install and use the latest version", - }, - { - name: ["--quiet", "-q"], - description: - "Suppress output from npx itself. Subcommands will not be affected", - }, - { - name: "--npm", - args: { - name: "path to binary", - template: "filepaths", - }, - description: "Npm binary to use for internal operations", - }, - { - args: {}, - description: "Extra node argument when calling a node binary", - name: ["--node-arg", "-n"], - }, - { - description: "Show version number", - name: ["--version", "-v"], - }, - { - description: "Show help", - name: ["--help", "-h"], - }, - ], -}; - -export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/nvm.ts b/extensions/terminal-suggest/src/completions/upstream/nvm.ts index 843ffef57de..6adafbcb174 100644 --- a/extensions/terminal-suggest/src/completions/upstream/nvm.ts +++ b/extensions/terminal-suggest/src/completions/upstream/nvm.ts @@ -28,10 +28,6 @@ const args: Fig.Arg = { isVariadic: true, }; -// const pattern: Fig.Arg = { -// name: "pattern", -// }; - const name: Fig.Arg = { name: "name", }; diff --git a/extensions/terminal-suggest/src/completions/upstream/pnpm.ts b/extensions/terminal-suggest/src/completions/upstream/pnpm.ts index 1710549572f..9ce7c798208 100644 --- a/extensions/terminal-suggest/src/completions/upstream/pnpm.ts +++ b/extensions/terminal-suggest/src/completions/upstream/pnpm.ts @@ -990,7 +990,8 @@ const completionSpec: Fig.Spec = { const { script, postProcess } = dependenciesGenerator as Fig.Generator & { script: string[]; }; - if (!postProcess) { + + if (postProcess === undefined) { return undefined; } @@ -1002,13 +1003,12 @@ const completionSpec: Fig.Spec = { }) ).stdout, tokens - )?.map((e: any) => e.name as string); - if (!packages) { - return undefined; - } + ) + ?.filter((e) => e !== null) + .map(({ name }) => name as string); const subcommands = packages - .filter((name) => nodeClis.has(name)) + ?.filter((name) => nodeClis.has(name)) .map((name) => ({ name, loadSpec: name, diff --git a/extensions/terminal-suggest/src/completions/upstream/ps.ts b/extensions/terminal-suggest/src/completions/upstream/ps.ts index 37c34815a8c..17dd6623679 100644 --- a/extensions/terminal-suggest/src/completions/upstream/ps.ts +++ b/extensions/terminal-suggest/src/completions/upstream/ps.ts @@ -1,153 +1,153 @@ const completionSpec: Fig.Spec = { - name: "ps", - description: "Report a snapshot of the current processes", - options: [ - { name: ["-A", "-e"], description: "Select all processes" }, - { - name: "-a", - description: "Select all processes except both session leaders", - args: { name: "getsid" }, - }, - { - name: "-d", - description: "Select all processes except session leaders", - }, - { - name: "--deselect", - description: - "Select all processes except those that fulfill the specified conditions", - }, - { - name: "-N", - description: - "Select all processes except those that fulfill the specified conditions (negates the selection)", - }, - { - name: "--pid", - description: "Select by process ID", - args: { name: "pidlist" }, - }, - { - name: "--ppid", - description: - "Select by parent process ID. This selects the processes with a parent process ID in pidlist", - args: { name: "pidlist" }, - }, - { - name: "--sid", - description: "Select by session ID", - args: { name: "sesslist" }, - }, - { - name: "--tty", - description: "Select by terminal", - args: { name: "ttylist" }, - }, - { - name: "U", - description: "Select by effective user ID (EUID) or name", - args: { name: "userlist" }, - }, - { - name: "-U", - description: "Select by real user ID (RUID) or name", - args: { name: "userlist" }, - }, - { - name: "-u", - description: "Select by effective user ID (EUID) or name", - args: { name: "userlist" }, - }, - { - name: "--User", - description: "Select by real user ID (RUID) or name", - args: { name: "userlist" }, - }, - { - name: "--user", - description: "Select by effective user ID (EUID) or name", - args: { name: "userlist" }, - }, - { - name: "-c", - description: "Show different scheduler information for the -l option", - }, - { - name: "--context", - description: "Display security context format (for SE Linux)", - }, - { name: "-f", description: "Do full-format listing" }, - { name: "-F", description: "Extra full format" }, - { - name: ["--format", "-o", "o"], - description: "", - args: { name: "format" }, - isRepeatable: true, - }, - { name: ["-M", "Z"], description: "(for SE Linux)" }, - { name: ["-y", "-l"], description: "" }, - { - name: "--cols", - description: "Set screen width", - args: { name: "n" }, - }, - { - name: "--columns", - description: "Set screen width", - args: { name: "n" }, - }, - { - name: "--cumulative", - description: - "Include some dead child process data (as a sum with the parent)", - }, - { name: "--forest", description: "ASCII art process tree" }, - { name: "-H", description: "Show process hierarchy (forest)" }, - { - name: "--headers", - description: "Repeat header lines, one per page of output", - }, - { - name: "-n", - description: "Set namelist file", - args: { name: "namelist" }, - }, - { - name: "--lines", - description: "Set screen height", - args: { name: "n" }, - }, - { - name: ["--no-headers", "--no-heading"], - description: "Print no header line at all", - }, - { - name: "--rows", - description: "Set screen height", - args: { name: "n" }, - }, - { - name: "--sort", - description: "Specify sorting order", - args: { name: "spec" }, - }, - { - name: "--width", - description: "Set screen width", - args: { name: "n" }, - }, - { - name: "-L", - description: "Show threads, possibly with LWP and NLWP columns", - }, - { - name: "-T", - description: "Show threads, possibly with SPID column", - }, - { name: "--help", description: "Print a help message" }, - { name: "--info", description: "Print debugging info" }, - { name: "--version", description: "Print the procps version" }, - ], + name: "ps", + description: "Report a snapshot of the current processes", + options: [ + { name: ["-A", "-e"], description: "Select all processes" }, + { + name: "-a", + description: "Select all processes except both session leaders", + args: { name: "getsid" }, + }, + { + name: "-d", + description: "Select all processes except session leaders", + }, + { + name: "--deselect", + description: + "Select all processes except those that fulfill the specified conditions", + }, + { + name: "-N", + description: + "Select all processes except those that fulfill the specified conditions (negates the selection)", + }, + { + name: "--pid", + description: "Select by process ID", + args: { name: "pidlist" }, + }, + { + name: "--ppid", + description: + "Select by parent process ID. This selects the processes with a parent process ID in pidlist", + args: { name: "pidlist" }, + }, + { + name: "--sid", + description: "Select by session ID", + args: { name: "sesslist" }, + }, + { + name: "--tty", + description: "Select by terminal", + args: { name: "ttylist" }, + }, + { + name: "U", + description: "Select by effective user ID (EUID) or name", + args: { name: "userlist" }, + }, + { + name: "-U", + description: "Select by real user ID (RUID) or name", + args: { name: "userlist" }, + }, + { + name: "-u", + description: "Select by effective user ID (EUID) or name", + args: { name: "userlist" }, + }, + { + name: "--User", + description: "Select by real user ID (RUID) or name", + args: { name: "userlist" }, + }, + { + name: "--user", + description: "Select by effective user ID (EUID) or name", + args: { name: "userlist" }, + }, + { + name: "-c", + description: "Show different scheduler information for the -l option", + }, + { + name: "--context", + description: "Display security context format (for SE Linux)", + }, + { name: "-f", description: "Do full-format listing" }, + { name: "-F", description: "Extra full format" }, + { + name: ["--format", "-o", "o"], + description: "", + args: { name: "format" }, + isRepeatable: true, + }, + { name: ["-M", "Z"], description: "(for SE Linux)" }, + { name: ["-y", "-l"], description: "" }, + { + name: "--cols", + description: "Set screen width", + args: { name: "n" }, + }, + { + name: "--columns", + description: "Set screen width", + args: { name: "n" }, + }, + { + name: "--cumulative", + description: + "Include some dead child process data (as a sum with the parent)", + }, + { name: "--forest", description: "ASCII art process tree" }, + { name: "-H", description: "Show process hierarchy (forest)" }, + { + name: "--headers", + description: "Repeat header lines, one per page of output", + }, + { + name: "-n", + description: "Set namelist file", + args: { name: "namelist" }, + }, + { + name: "--lines", + description: "Set screen height", + args: { name: "n" }, + }, + { + name: ["--no-headers", "--no-heading"], + description: "Print no header line at all", + }, + { + name: "--rows", + description: "Set screen height", + args: { name: "n" }, + }, + { + name: "--sort", + description: "Specify sorting order", + args: { name: "spec" }, + }, + { + name: "--width", + description: "Set screen width", + args: { name: "n" }, + }, + { + name: "-L", + description: "Show threads, possibly with LWP and NLWP columns", + }, + { + name: "-T", + description: "Show threads, possibly with SPID column", + }, + { name: "--help", description: "Print a help message" }, + { name: "--info", description: "Print debugging info" }, + { name: "--version", description: "Print the procps version" }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/pwd.ts b/extensions/terminal-suggest/src/completions/upstream/pwd.ts index 42126db30b7..11c390ad031 100644 --- a/extensions/terminal-suggest/src/completions/upstream/pwd.ts +++ b/extensions/terminal-suggest/src/completions/upstream/pwd.ts @@ -1,16 +1,16 @@ const completionSpec: Fig.Spec = { - name: "pwd", - description: "Return working directory name", - options: [ - { - name: "-L", - description: "Display the logical current working directory", - }, - { - name: "-P", - description: "Display the physical current working directory", - }, - ], + name: "pwd", + description: "Return working directory name", + options: [ + { + name: "-L", + description: "Display the logical current working directory", + }, + { + name: "-P", + description: "Display the physical current working directory", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/python.ts b/extensions/terminal-suggest/src/completions/upstream/python.ts index 2374ee6b5e8..beca86b3bee 100644 --- a/extensions/terminal-suggest/src/completions/upstream/python.ts +++ b/extensions/terminal-suggest/src/completions/upstream/python.ts @@ -21,6 +21,7 @@ const completionSpec: Fig.Spec = { }, args: { name: "python script", + isScript: true, generators: filepaths({ extensions: ["py"], editFileSuggestions: { priority: 76 }, diff --git a/extensions/terminal-suggest/src/completions/upstream/rm.ts b/extensions/terminal-suggest/src/completions/upstream/rm.ts index 7b52f909527..9aaf18553fe 100644 --- a/extensions/terminal-suggest/src/completions/upstream/rm.ts +++ b/extensions/terminal-suggest/src/completions/upstream/rm.ts @@ -1,43 +1,43 @@ const completionSpec: Fig.Spec = { - name: "rm", - description: "Remove directory entries", - args: { - isVariadic: true, - template: ["folders", "filepaths"], - }, + name: "rm", + description: "Remove directory entries", + args: { + isVariadic: true, + template: ["folders", "filepaths"], + }, - options: [ - { - name: ["-r", "-R"], - description: - "Recursive. Attempt to remove the file hierarchy rooted in each file argument", - isDangerous: true, - }, - { - name: "-P", - description: "Overwrite regular files before deleting them", - isDangerous: true, - }, - { - name: "-d", - description: - "Attempt to remove directories as well as other types of files", - }, - { - name: "-f", - description: - "⚠️ Attempt to remove the files without prompting for confirmation", - isDangerous: true, - }, - { - name: "-i", - description: "Request confirmation before attempting to remove each file", - }, - { - name: "-v", - description: "Be verbose when deleting files", - }, - ], + options: [ + { + name: ["-r", "-R"], + description: + "Recursive. Attempt to remove the file hierarchy rooted in each file argument", + isDangerous: true, + }, + { + name: "-P", + description: "Overwrite regular files before deleting them", + isDangerous: true, + }, + { + name: "-d", + description: + "Attempt to remove directories as well as other types of files", + }, + { + name: "-f", + description: + "⚠️ Attempt to remove the files without prompting for confirmation", + isDangerous: true, + }, + { + name: "-i", + description: "Request confirmation before attempting to remove each file", + }, + { + name: "-v", + description: "Be verbose when deleting files", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/rmdir.ts b/extensions/terminal-suggest/src/completions/upstream/rmdir.ts index 92790e75d0d..430b9a678ef 100644 --- a/extensions/terminal-suggest/src/completions/upstream/rmdir.ts +++ b/extensions/terminal-suggest/src/completions/upstream/rmdir.ts @@ -1,18 +1,18 @@ const completionSpec: Fig.Spec = { - name: "rmdir", - description: "Remove directories", - args: { - isVariadic: true, - template: "folders", - }, + name: "rmdir", + description: "Remove directories", + args: { + isVariadic: true, + template: "folders", + }, - options: [ - { - name: "-p", - description: "Remove each directory of path", - isDangerous: true, - }, - ], + options: [ + { + name: "-p", + description: "Remove each directory of path", + isDangerous: true, + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/scp.ts b/extensions/terminal-suggest/src/completions/upstream/scp.ts index 93da019b686..3971e94d46f 100644 --- a/extensions/terminal-suggest/src/completions/upstream/scp.ts +++ b/extensions/terminal-suggest/src/completions/upstream/scp.ts @@ -1,226 +1,226 @@ import { knownHosts, configHosts } from "./ssh"; const completionSpec: Fig.Spec = { - name: "scp", - description: "Copies files or directories between hosts on a network", - args: [ - { - name: "sources", - description: "File or directory, local or remote ([user@]host:[path])", - isVariadic: true, - generators: [ - knownHosts, - configHosts, - { template: ["history", "filepaths", "folders"] }, - ], - }, - { - name: "target", - description: "File or directory, local or remote ([user@]host:[path])", - generators: [ - knownHosts, - configHosts, - { template: ["history", "filepaths", "folders"] }, - ], - }, - ], - options: [ - { - name: "-3", - description: `Copies between two remote hosts are transferred through the local + name: "scp", + description: "Copies files or directories between hosts on a network", + args: [ + { + name: "sources", + description: "File or directory, local or remote ([user@]host:[path])", + isVariadic: true, + generators: [ + knownHosts, + configHosts, + { template: ["history", "filepaths", "folders"] }, + ], + }, + { + name: "target", + description: "File or directory, local or remote ([user@]host:[path])", + generators: [ + knownHosts, + configHosts, + { template: ["history", "filepaths", "folders"] }, + ], + }, + ], + options: [ + { + name: "-3", + description: `Copies between two remote hosts are transferred through the local host. Without this option the data is copied directly between the two remote hosts. Note that this option disables the progress meter and selects batch mode for the second host, since scp cannot ask for passwords or passphrases for both hosts`, - }, - { - name: "-4", - description: "Forces scp to use IPv4 addresses only", - }, - { - name: "-6", - description: "Forces scp to use IPv6 addresses only", - }, - { - name: "-A", - description: - "Allows forwarding of ssh-agent(1) to the remote system. The default is not to forward an authentication agent", - }, - { - name: "-B", - description: - "Selects batch mode (prevents asking for passwords or passphrases)", - }, - { - name: "-C", - description: - "Compression enable. Passes the -C flag to ssh(1) to enable compression", - }, - { - name: "-c", - description: - "Selects the cipher to use for encrypting the data transfer. This option is directly passed to ssh(1)", - args: { - name: "cipher", - description: "The selected cipher specification", - }, - }, - { - name: "-F", - description: - "Specifies an alternative per-user configuration file for ssh. This option is directly passed to ssh(1)", - args: { - name: "ssh_config", - description: "The selected ssh config", - }, - }, - { - name: "-i", - description: - "Selects the file from which the identity (private key) for public key authentication is read. This option is directly passed to ssh(1)", - args: { - name: "identity_file", - description: "Specified identity file", - }, - }, - { - name: "-J", - description: `Connect to the target host by first making an scp connection to the + }, + { + name: "-4", + description: "Forces scp to use IPv4 addresses only", + }, + { + name: "-6", + description: "Forces scp to use IPv6 addresses only", + }, + { + name: "-A", + description: + "Allows forwarding of ssh-agent(1) to the remote system. The default is not to forward an authentication agent", + }, + { + name: "-B", + description: + "Selects batch mode (prevents asking for passwords or passphrases)", + }, + { + name: "-C", + description: + "Compression enable. Passes the -C flag to ssh(1) to enable compression", + }, + { + name: "-c", + description: + "Selects the cipher to use for encrypting the data transfer. This option is directly passed to ssh(1)", + args: { + name: "cipher", + description: "The selected cipher specification", + }, + }, + { + name: "-F", + description: + "Specifies an alternative per-user configuration file for ssh. This option is directly passed to ssh(1)", + args: { + name: "ssh_config", + description: "The selected ssh config", + }, + }, + { + name: "-i", + description: + "Selects the file from which the identity (private key) for public key authentication is read. This option is directly passed to ssh(1)", + args: { + name: "identity_file", + description: "Specified identity file", + }, + }, + { + name: "-J", + description: `Connect to the target host by first making an scp connection to the jump host described by destination and then establishing a TCP forwarding to the ultimate destination from there. Multiple jump hops may be specified separated by comma characters. This is a shortcut to specify a ProxyJump configuration directive. This option is directly passed to ssh(1)`, - args: { - name: "destination", - description: "Scp destination", - }, - }, - { - name: "-l", - description: "Limits the used bandwidth, specified in Kbit/s", - args: { - name: "limit", - description: "Limit bandwidth in Kbit/s", - }, - }, - { - name: "-o", - description: `Can be used to pass options to ssh in the format used in + args: { + name: "destination", + description: "Scp destination", + }, + }, + { + name: "-l", + description: "Limits the used bandwidth, specified in Kbit/s", + args: { + name: "limit", + description: "Limit bandwidth in Kbit/s", + }, + }, + { + name: "-o", + description: `Can be used to pass options to ssh in the format used in ssh_config(5). This is useful for specifying options for which there is no separate scp command-line flag. For full details of the options listed below, and their possible values, see ssh_config(5)`, - args: { - name: "option", - suggestions: [ - { name: "AddressFamily" }, - { name: "BatchMode" }, - { name: "BindAddress" }, - { name: "ChallengeResponseAuthentication" }, - { name: "CheckHostIP" }, - { name: "Cipher" }, - { name: "Ciphers" }, - { name: "ClearAllForwardings" }, - { name: "Compression" }, - { name: "CompressionLevel" }, - { name: "ConnectionAttempts" }, - { name: "ConnectTimeout" }, - { name: "ControlMaster" }, - { name: "ControlPath" }, - { name: "ControlPersist" }, - { name: "DynamicForward" }, - { name: "EscapeChar" }, - { name: "ExitOnForwardFailure" }, - { name: "ForwardAgent" }, - { name: "ForwardX11" }, - { name: "ForwardX11Timeout" }, - { name: "ForwardX11Trusted" }, - { name: "GatewayPorts" }, - { name: "GlobalKnownHostsFile" }, - { name: "GSSAPIAuthentication" }, - { name: "GSSAPIDelegateCredentials" }, - { name: "HashKnownHosts" }, - { name: "Host" }, - { name: "HostbasedAuthentication" }, - { name: "HostKeyAlgorithms" }, - { name: "HostKeyAlias" }, - { name: "HostName" }, - { name: "IdentityFile" }, - { name: "IdentitiesOnly" }, - { name: "IPQoS" }, - { name: "KbdInteractiveAuthentication" }, - { name: "KbdInteractiveDevices" }, - { name: "KexAlgorithms" }, - { name: "LocalCommand" }, - { name: "LocalForward" }, - { name: "LogLevel" }, - { name: "MACs" }, - { name: "NoHostAuthenticationForLocalhost" }, - { name: "NumberOfPasswordPrompts" }, - { name: "PasswordAuthentication" }, - { name: "PermitLocalCommand" }, - { name: "PKCS11Provider" }, - { name: "Port" }, - { name: "PreferredAuthentications" }, - { name: "Protocol" }, - { name: "ProxyCommand" }, - { name: "PubkeyAuthentication" }, - { name: "RekeyLimit" }, - { name: "RequestTTY" }, - { name: "RhostsRSAAuthentication" }, - { name: "RSAAuthentication" }, - { name: "SendEnv" }, - { name: "ServerAliveInterval" }, - { name: "ServerAliveCountMax" }, - { name: "StrictHostKeyChecking" }, - { name: "TCPKeepAlive" }, - { name: "Tunnel" }, - { name: "TunnelDevice" }, - { name: "UsePrivilegedPort" }, - { name: "User" }, - { name: "UserKnownHostsFile" }, - { name: "VerifyHostKeyDNS" }, - { name: "VisualHostKey" }, - { name: "XAuthLocation" }, - ], - }, - }, - { - name: "-P", - description: `Specifies the port to connect to on the remote host. Note that + args: { + name: "option", + suggestions: [ + { name: "AddressFamily" }, + { name: "BatchMode" }, + { name: "BindAddress" }, + { name: "ChallengeResponseAuthentication" }, + { name: "CheckHostIP" }, + { name: "Cipher" }, + { name: "Ciphers" }, + { name: "ClearAllForwardings" }, + { name: "Compression" }, + { name: "CompressionLevel" }, + { name: "ConnectionAttempts" }, + { name: "ConnectTimeout" }, + { name: "ControlMaster" }, + { name: "ControlPath" }, + { name: "ControlPersist" }, + { name: "DynamicForward" }, + { name: "EscapeChar" }, + { name: "ExitOnForwardFailure" }, + { name: "ForwardAgent" }, + { name: "ForwardX11" }, + { name: "ForwardX11Timeout" }, + { name: "ForwardX11Trusted" }, + { name: "GatewayPorts" }, + { name: "GlobalKnownHostsFile" }, + { name: "GSSAPIAuthentication" }, + { name: "GSSAPIDelegateCredentials" }, + { name: "HashKnownHosts" }, + { name: "Host" }, + { name: "HostbasedAuthentication" }, + { name: "HostKeyAlgorithms" }, + { name: "HostKeyAlias" }, + { name: "HostName" }, + { name: "IdentityFile" }, + { name: "IdentitiesOnly" }, + { name: "IPQoS" }, + { name: "KbdInteractiveAuthentication" }, + { name: "KbdInteractiveDevices" }, + { name: "KexAlgorithms" }, + { name: "LocalCommand" }, + { name: "LocalForward" }, + { name: "LogLevel" }, + { name: "MACs" }, + { name: "NoHostAuthenticationForLocalhost" }, + { name: "NumberOfPasswordPrompts" }, + { name: "PasswordAuthentication" }, + { name: "PermitLocalCommand" }, + { name: "PKCS11Provider" }, + { name: "Port" }, + { name: "PreferredAuthentications" }, + { name: "Protocol" }, + { name: "ProxyCommand" }, + { name: "PubkeyAuthentication" }, + { name: "RekeyLimit" }, + { name: "RequestTTY" }, + { name: "RhostsRSAAuthentication" }, + { name: "RSAAuthentication" }, + { name: "SendEnv" }, + { name: "ServerAliveInterval" }, + { name: "ServerAliveCountMax" }, + { name: "StrictHostKeyChecking" }, + { name: "TCPKeepAlive" }, + { name: "Tunnel" }, + { name: "TunnelDevice" }, + { name: "UsePrivilegedPort" }, + { name: "User" }, + { name: "UserKnownHostsFile" }, + { name: "VerifyHostKeyDNS" }, + { name: "VisualHostKey" }, + { name: "XAuthLocation" }, + ], + }, + }, + { + name: "-P", + description: `Specifies the port to connect to on the remote host. Note that this option is written with a capital ‘P’, because -p is already reserved for preserving the times and modes of the file`, - args: { - name: "port", - }, - }, - { - name: "-p", - description: - "Preserves modification times, access times, and modes from the original file", - }, - { - name: "-q", - description: - "Quiet mode: disables the progress meter as well as warning and diagnostic messages from ssh(1)", - }, - { - name: "-r", - description: - "Recursively copy entire directories. Note that scp follows symbolic links encountered in the tree traversal", - }, - { - name: "-S", - description: - "Name of program to use for the encrypted connection. The program must understand ssh(1) options", - args: { - name: "program", - }, - }, - { - name: "-T", - description: `Disable strict filename checking. By default when copying files + args: { + name: "port", + }, + }, + { + name: "-p", + description: + "Preserves modification times, access times, and modes from the original file", + }, + { + name: "-q", + description: + "Quiet mode: disables the progress meter as well as warning and diagnostic messages from ssh(1)", + }, + { + name: "-r", + description: + "Recursively copy entire directories. Note that scp follows symbolic links encountered in the tree traversal", + }, + { + name: "-S", + description: + "Name of program to use for the encrypted connection. The program must understand ssh(1) options", + args: { + name: "program", + }, + }, + { + name: "-T", + description: `Disable strict filename checking. By default when copying files from a remote host to a local directory scp checks that the received filenames match those requested on the command-line to prevent the remote end from sending unexpected or unwanted files. @@ -229,13 +229,13 @@ interpret filename wildcards, these checks may cause wanted files to be rejected. This option disables these checks at the expense of fully trusting that the server will not send unexpected filenames`, - }, - { - name: "-v", - description: - "Verbose mode. Causes scp and ssh(1) to print debugging messages about their progress. This is helpful in debugging connection, authentication, and configuration problems", - }, - ], + }, + { + name: "-v", + description: + "Verbose mode. Causes scp and ssh(1) to print debugging messages about their progress. This is helpful in debugging connection, authentication, and configuration problems", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/ssh.ts b/extensions/terminal-suggest/src/completions/upstream/ssh.ts index 9b0f5bd4e77..4998530d3ec 100644 --- a/extensions/terminal-suggest/src/completions/upstream/ssh.ts +++ b/extensions/terminal-suggest/src/completions/upstream/ssh.ts @@ -43,7 +43,7 @@ const getConfigLines = async ( .map((line) => line.split(" ")[1]); // Get the lines of every include file - const includeLines: any = await Promise.all( + const includeLines: string[][] = await Promise.all( includes.map((file) => getConfigLines(file, executeShellCommand, home, basePath) ) @@ -89,10 +89,10 @@ export const configHosts: Fig.Generator = { return configLines .filter( - (line: any) => + (line) => line.trim().toLowerCase().startsWith("host ") && !line.includes("*") ) - .map((host: any) => ({ + .map((host) => ({ name: host.split(" ")[1], description: "SSH host", priority: 90, diff --git a/extensions/terminal-suggest/src/completions/upstream/tail.ts b/extensions/terminal-suggest/src/completions/upstream/tail.ts index 657c610b37d..fd905588c60 100644 --- a/extensions/terminal-suggest/src/completions/upstream/tail.ts +++ b/extensions/terminal-suggest/src/completions/upstream/tail.ts @@ -1,20 +1,20 @@ const completionSpec: Fig.Spec = { - name: "tail", - description: "Display the last part of a file", - args: { - isVariadic: true, - template: "filepaths", - }, - options: [ - { - name: "-f", - description: "Wait for additional data to be appended", - }, - { - name: "-r", - description: "Display in reverse order", - }, - ], + name: "tail", + description: "Display the last part of a file", + args: { + isVariadic: true, + template: "filepaths", + }, + options: [ + { + name: "-f", + description: "Wait for additional data to be appended", + }, + { + name: "-r", + description: "Display in reverse order", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/top.ts b/extensions/terminal-suggest/src/completions/upstream/top.ts index df922164c06..5831fd7b89e 100644 --- a/extensions/terminal-suggest/src/completions/upstream/top.ts +++ b/extensions/terminal-suggest/src/completions/upstream/top.ts @@ -1,49 +1,49 @@ const completionSpec: Fig.Spec = { - name: "top", - description: "Display Linux tasks", - options: [ - { - name: ["-h", "-v"], - description: "Show library version and usage prompt", - }, - { - name: "-b", - description: "Starts top in Batch mode", - args: { - name: "operation", - }, - }, - { - name: "-c", - description: "Starts top with last remembered c state reversed", - args: { - name: "toggle", - }, - }, - { - name: "-i", - description: - "Starts top with the last remembered 'i' state reversed. When this toggle is Off, tasks that are idled or zombied will not be displayed", - args: { - name: "toggle", - }, - }, - { - name: "-s", - description: "Starts top with secure mode forced", - args: { - name: "delay", - }, - }, - { - name: "-pid", - description: "Monitor pids", - args: { - name: "process ids", - isVariadic: true, - }, - }, - ], + name: "top", + description: "Display Linux tasks", + options: [ + { + name: ["-h", "-v"], + description: "Show library version and usage prompt", + }, + { + name: "-b", + description: "Starts top in Batch mode", + args: { + name: "operation", + }, + }, + { + name: "-c", + description: "Starts top with last remembered c state reversed", + args: { + name: "toggle", + }, + }, + { + name: "-i", + description: + "Starts top with the last remembered 'i' state reversed. When this toggle is Off, tasks that are idled or zombied will not be displayed", + args: { + name: "toggle", + }, + }, + { + name: "-s", + description: "Starts top with secure mode forced", + args: { + name: "delay", + }, + }, + { + name: "-pid", + description: "Monitor pids", + args: { + name: "process ids", + isVariadic: true, + }, + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/touch.ts b/extensions/terminal-suggest/src/completions/upstream/touch.ts index 45208878313..6987272ad84 100644 --- a/extensions/terminal-suggest/src/completions/upstream/touch.ts +++ b/extensions/terminal-suggest/src/completions/upstream/touch.ts @@ -1,59 +1,59 @@ const completionSpec: Fig.Spec = { - name: "touch", - description: "Change file access and modification times", - args: { - name: "file", - isVariadic: true, - template: "folders", - suggestCurrentToken: true, - }, - options: [ - { - name: "-A", - description: - "Adjust the access and modification time stamps for the file by the specified value", - args: { - name: "time", - description: "[-][[hh]mm]SS", - }, - }, - { name: "-a", description: "Change the access time of the file" }, - { - name: "-c", - description: "Do not create the file if it does not exist", - }, - { - name: "-f", - description: - "Attempt to force the update, even if the file permissions do not currently permit it", - }, - { - name: "-h", - description: - "If the file is a symbolic link, change the times of the link itself rather than the file that the link points to", - }, - { - name: "-m", - description: "Change the modification time of the file", - }, - { - name: "-r", - description: - "Use the access and modifications times from the specified file instead of the current time of day", - args: { - name: "file", - }, - }, - { - name: "-t", - description: - "Change the access and modification times to the specified time instead of the current time of day", - args: { - name: "timestamp", - description: "[[CC]YY]MMDDhhmm[.SS]", - }, - }, - ], + name: "touch", + description: "Change file access and modification times", + args: { + name: "file", + isVariadic: true, + template: "folders", + suggestCurrentToken: true, + }, + options: [ + { + name: "-A", + description: + "Adjust the access and modification time stamps for the file by the specified value", + args: { + name: "time", + description: "[-][[hh]mm]SS", + }, + }, + { name: "-a", description: "Change the access time of the file" }, + { + name: "-c", + description: "Do not create the file if it does not exist", + }, + { + name: "-f", + description: + "Attempt to force the update, even if the file permissions do not currently permit it", + }, + { + name: "-h", + description: + "If the file is a symbolic link, change the times of the link itself rather than the file that the link points to", + }, + { + name: "-m", + description: "Change the modification time of the file", + }, + { + name: "-r", + description: + "Use the access and modifications times from the specified file instead of the current time of day", + args: { + name: "file", + }, + }, + { + name: "-t", + description: + "Change the access and modification times to the specified time instead of the current time of day", + args: { + name: "timestamp", + description: "[[CC]YY]MMDDhhmm[.SS]", + }, + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/uname.ts b/extensions/terminal-suggest/src/completions/upstream/uname.ts index e73b9efe397..775387703d4 100644 --- a/extensions/terminal-suggest/src/completions/upstream/uname.ts +++ b/extensions/terminal-suggest/src/completions/upstream/uname.ts @@ -1,36 +1,36 @@ const completionSpec: Fig.Spec = { - name: "uname", - description: "Print operating system name", - options: [ - { - name: "-a", - description: "Print all available system information", - }, - { - name: "-m", - description: "Print the machine hardware name", - }, - { - name: "-n", - description: "Print the system hostname", - }, - { - name: "-p", - description: "Print the machine processor architecture name", - }, - { - name: "-r", - description: "Print the operating system release", - }, - { - name: "-s", - description: "Print the operating system name", - }, - { - name: "-v", - description: "Print the operating system version", - }, - ], + name: "uname", + description: "Print operating system name", + options: [ + { + name: "-a", + description: "Print all available system information", + }, + { + name: "-m", + description: "Print the machine hardware name", + }, + { + name: "-n", + description: "Print the system hostname", + }, + { + name: "-p", + description: "Print the machine processor architecture name", + }, + { + name: "-r", + description: "Print the operating system release", + }, + { + name: "-s", + description: "Print the operating system name", + }, + { + name: "-v", + description: "Print the operating system version", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/vim.ts b/extensions/terminal-suggest/src/completions/upstream/vim.ts index 7e41109f025..267edb385ff 100644 --- a/extensions/terminal-suggest/src/completions/upstream/vim.ts +++ b/extensions/terminal-suggest/src/completions/upstream/vim.ts @@ -1,244 +1,244 @@ const completionSpec: Fig.Spec = { - name: "vim", - description: "Vi IMproved, a programmer's text editor", - args: { - template: "filepaths", - // suggestCurrentToken: true, - }, - options: [ - { - name: "-v", - description: "Vi mode (like 'vi')", - }, - { - name: "-e", - description: "Ex mode (like 'ex')", - }, - { - name: "-E", - description: "Improved Ex mode", - }, - { - name: "-s", - description: - "Enable silent mode (when in ex mode), or Read Normal mode commands from file", - args: { - name: "scriptin", - template: "filepaths", - isOptional: true, - }, - }, - { - name: "-d", - description: "Diff mode (like 'vimdiff')", - }, - { - name: "-y", - description: "Easy mode (like 'evim', modeless)", - }, - { - name: "-R", - description: "Readonly mode (like 'view')", - }, - { - name: "-Z", - description: "Restricted mode (like 'rvim')", - }, - { - name: "-m", - description: "Modifications (writing files) not allowed", - }, - { - name: "-M", - description: "Modifications in text not allowed", - }, - { - name: "-b", - description: "Binary mode", - }, - { - name: "-l", - description: "Lisp mode", - }, - { - name: "-C", - description: "Compatible with Vi: 'compatible'", - }, - { - name: "-N", - description: "Not fully Vi compatible: 'nocompatible'", - }, - { - name: "-V", - description: "Be verbose [level N] [log messages to fname]", - args: [ - { - name: "N", - }, - { - name: "fname", - template: "filepaths", - }, - ], - }, - { - name: "-D", - description: "Debugging mode", - }, - { - name: "-n", - description: "No swap file, use memory only", - }, - { - name: "-r", - description: - "Recover crashed session if filename is specified, otherwise list swap files and exit", - args: { - name: "filename", - isOptional: true, - template: "filepaths", - }, - }, - { - name: "-L", - description: "Same as -r", - args: { - name: "filename", - template: "filepaths", - }, - }, - { - name: "-T", - description: "Set terminal type to ", - args: { - name: "terminal", - }, - }, - { - name: "--not-a-term", - description: "Skip warning for input/output not being a terminal", - }, - { - name: "--ttyfail", - description: "Exit if input or output is not a terminal", - }, - { - name: "-u", - description: "Use instead of any .vimrc", - args: { - name: "vimrc", - template: "filepaths", - }, - }, - { - name: "--noplugin", - description: "Don't load plugin scripts", - }, - { - name: "-p", - description: "Open N tab pages (default: one for each file)", - args: { - name: "N", - isOptional: true, - }, - }, - { - name: "-o", - description: "Open N windows (default: one for each file)", - args: { - name: "N", - isOptional: true, - }, - }, - { - name: "-O", - description: "Like -o but split vertically", - args: { - name: "N", - isOptional: true, - }, - }, - { - name: "+", - description: - "Start at end of file, if line number is specified, start at that line", - args: { - name: "lnum", - isOptional: true, - }, - }, - { - name: "--cmd", - description: "Execute before loading any vimrc file", - args: { - name: "command", - isCommand: true, - }, - }, - { - name: "-c", - description: "Execute after loading the first file", - args: { - name: "command", - }, - }, - { - name: "-S", - description: "Source file after loading the first file", - args: { - name: "session", - template: "filepaths", - }, - }, - { - name: "-w", - description: "Append all typed commands to file ", - args: { - name: "scriptout", - template: "filepaths", - }, - }, - { - name: "-W", - description: "Write all typed commands to file ", - args: { - name: "scriptout", - template: "filepaths", - }, - }, - { - name: "-x", - description: "Edit encrypted files", - }, - { - name: "--startuptime", - description: "Write startup timing messages to ", - args: { - name: "file", - template: "filepaths", - }, - }, - { - name: "-i", - description: "Use instead of .viminfo", - args: { - name: "viminfo", - template: "filepaths", - }, - }, - { - name: "--clean", - description: "'nocompatible', Vim defaults, no plugins, no viminfo", - }, - { - name: ["-h", "--help"], - description: "Print Help message and exit", - }, - { - name: "--version", - description: "Print version information and exit", - }, - ], + name: "vim", + description: "Vi IMproved, a programmer's text editor", + args: { + template: "filepaths", + // suggestCurrentToken: true, + }, + options: [ + { + name: "-v", + description: "Vi mode (like 'vi')", + }, + { + name: "-e", + description: "Ex mode (like 'ex')", + }, + { + name: "-E", + description: "Improved Ex mode", + }, + { + name: "-s", + description: + "Enable silent mode (when in ex mode), or Read Normal mode commands from file", + args: { + name: "scriptin", + template: "filepaths", + isOptional: true, + }, + }, + { + name: "-d", + description: "Diff mode (like 'vimdiff')", + }, + { + name: "-y", + description: "Easy mode (like 'evim', modeless)", + }, + { + name: "-R", + description: "Readonly mode (like 'view')", + }, + { + name: "-Z", + description: "Restricted mode (like 'rvim')", + }, + { + name: "-m", + description: "Modifications (writing files) not allowed", + }, + { + name: "-M", + description: "Modifications in text not allowed", + }, + { + name: "-b", + description: "Binary mode", + }, + { + name: "-l", + description: "Lisp mode", + }, + { + name: "-C", + description: "Compatible with Vi: 'compatible'", + }, + { + name: "-N", + description: "Not fully Vi compatible: 'nocompatible'", + }, + { + name: "-V", + description: "Be verbose [level N] [log messages to fname]", + args: [ + { + name: "N", + }, + { + name: "fname", + template: "filepaths", + }, + ], + }, + { + name: "-D", + description: "Debugging mode", + }, + { + name: "-n", + description: "No swap file, use memory only", + }, + { + name: "-r", + description: + "Recover crashed session if filename is specified, otherwise list swap files and exit", + args: { + name: "filename", + isOptional: true, + template: "filepaths", + }, + }, + { + name: "-L", + description: "Same as -r", + args: { + name: "filename", + template: "filepaths", + }, + }, + { + name: "-T", + description: "Set terminal type to ", + args: { + name: "terminal", + }, + }, + { + name: "--not-a-term", + description: "Skip warning for input/output not being a terminal", + }, + { + name: "--ttyfail", + description: "Exit if input or output is not a terminal", + }, + { + name: "-u", + description: "Use instead of any .vimrc", + args: { + name: "vimrc", + template: "filepaths", + }, + }, + { + name: "--noplugin", + description: "Don't load plugin scripts", + }, + { + name: "-p", + description: "Open N tab pages (default: one for each file)", + args: { + name: "N", + isOptional: true, + }, + }, + { + name: "-o", + description: "Open N windows (default: one for each file)", + args: { + name: "N", + isOptional: true, + }, + }, + { + name: "-O", + description: "Like -o but split vertically", + args: { + name: "N", + isOptional: true, + }, + }, + { + name: "+", + description: + "Start at end of file, if line number is specified, start at that line", + args: { + name: "lnum", + isOptional: true, + }, + }, + { + name: "--cmd", + description: "Execute before loading any vimrc file", + args: { + name: "command", + isCommand: true, + }, + }, + { + name: "-c", + description: "Execute after loading the first file", + args: { + name: "command", + }, + }, + { + name: "-S", + description: "Source file after loading the first file", + args: { + name: "session", + template: "filepaths", + }, + }, + { + name: "-w", + description: "Append all typed commands to file ", + args: { + name: "scriptout", + template: "filepaths", + }, + }, + { + name: "-W", + description: "Write all typed commands to file ", + args: { + name: "scriptout", + template: "filepaths", + }, + }, + { + name: "-x", + description: "Edit encrypted files", + }, + { + name: "--startuptime", + description: "Write startup timing messages to ", + args: { + name: "file", + template: "filepaths", + }, + }, + { + name: "-i", + description: "Use instead of .viminfo", + args: { + name: "viminfo", + template: "filepaths", + }, + }, + { + name: "--clean", + description: "'nocompatible', Vim defaults, no plugins, no viminfo", + }, + { + name: ["-h", "--help"], + description: "Print Help message and exit", + }, + { + name: "--version", + description: "Print version information and exit", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/wget.ts b/extensions/terminal-suggest/src/completions/upstream/wget.ts index fd8442d9e8a..ff14b34f0f8 100644 --- a/extensions/terminal-suggest/src/completions/upstream/wget.ts +++ b/extensions/terminal-suggest/src/completions/upstream/wget.ts @@ -1,399 +1,399 @@ const completionSpec: Fig.Spec = { - name: "wget", - description: "A non-interactive network retriever", - args: { - isVariadic: true, - name: "url", - description: "The url(s) to retrieve", - }, - options: [ - { - name: ["-V", "--version"], - description: "Display the version of Wget and exit", - }, - { name: ["-h", "--help"], description: "Print this help" }, - { - name: ["-b", "--background"], - description: "Go to background after startup", - }, - { - name: ["-e", "--execute=COMMAND"], - description: "Execute a `.wgetrc'-style command", - }, - { name: ["-o", "--output-file=FILE"], description: "Log messages to FILE" }, - { - name: ["-a", "--append-output=FILE"], - description: "Append messages to FILE", - }, - { name: ["-q", "--quiet"], description: "Quiet (no output)" }, - { - name: ["-v", "--verbose"], - description: "Be verbose (this is the default)", - }, - { - name: ["-nv", "--no-verbose"], - description: "Turn off verboseness, without being quiet", - }, - { - name: "--report-speed=TYPE", - description: "Output bandwidth as TYPE. TYPE can be bits", - }, - { - name: ["-i", "--input-file=FILE"], - description: "Download URLs found in local or external FILE", - }, - { name: ["-F", "--force-html"], description: "Treat input file as HTML" }, - { - name: ["-B", "--base=URL"], - description: "Resolves HTML input-file links (-i -F) relative to URL", - }, - { name: "--config=FILE", description: "Specify config file to use" }, - { name: "--no-config", description: "Do not read any config file" }, - { - name: "--rejected-log=FILE", - description: "Log reasons for URL rejection to FILE", - }, - { - name: ["-t", "--tries=NUMBER"], - description: "Set number of retries to NUMBER (0 unlimits)", - }, - { - name: "--retry-connrefused", - description: "Retry even if connection is refused", - }, - { - name: "--retry-on-http-error", - description: "Comma-separated list of HTTP errors to retry", - }, - { - name: ["-O", "--output-document=FILE"], - description: "Write documents to FILE", - }, - { - name: ["-nc", "--no-clobber"], - description: - "Skip downloads that would download to existing files (overwriting them)", - }, - { - name: "--no-netrc", - description: "Don't try to obtain credentials from .netrc", - }, - { - name: ["-c", "--continue"], - description: "Resume getting a partially-downloaded file", - }, - { - name: "--start-pos=OFFSET", - description: "Start downloading from zero-based position OFFSET", - }, - { name: "--progress=TYPE", description: "Select progress gauge type" }, - { - name: "--show-progress", - description: "Display the progress bar in any verbosity mode", - }, - { - name: ["-N", "--timestamping"], - description: "Don't re-retrieve files unless newer than local", - }, - { name: ["-S", "--server-response"], description: "Print server response" }, - { name: "--spider", description: "Don't download anything" }, - { - name: ["-T", "--timeout=SECONDS"], - description: "Set all timeout values to SECONDS", - }, - { - name: "--dns-timeout=SECS", - description: "Set the DNS lookup timeout to SECS", - }, - { - name: "--connect-timeout=SECS", - description: "Set the connect timeout to SECS", - }, - { - name: "--read-timeout=SECS", - description: "Set the read timeout to SECS", - }, - { - name: ["-w", "--wait=SECONDS"], - description: "Wait SECONDS between retrievals", - }, - { - name: "--waitretry=SECONDS", - description: "Wait 1..SECONDS between retries of a retrieval", - }, - { - name: "--random-wait", - description: "Wait from 0.5*WAIT...1.5*WAIT secs between retrievals", - }, - { name: "--no-proxy", description: "Explicitly turn off proxy" }, - { - name: ["-Q", "--quota=NUMBER"], - description: "Set retrieval quota to NUMBER", - }, - { - name: "--bind-address=ADDRESS", - description: "Bind to ADDRESS (hostname or IP) on local host", - }, - { name: "--limit-rate=RATE", description: "Limit download rate to RATE" }, - { name: "--no-dns-cache", description: "Disable caching DNS lookups" }, - { - name: "--restrict-file-names=OS", - description: "Restrict chars in file names to ones OS allows", - }, - { - name: "--ignore-case", - description: "Ignore case when matching files/directories", - }, - { - name: ["-4", "--inet4-only"], - description: "Connect only to IPv4 addresses", - }, - { - name: ["-6", "--inet6-only"], - description: "Connect only to IPv6 addresses", - }, - { - name: "--user=USER", - description: "Set both ftp and http user to USER", - }, - { - name: "--password=PASS", - description: "Set both ftp and http password to PASS", - }, - { name: "--ask-password", description: "Prompt for passwords" }, - { name: "--no-iri", description: "Turn off IRI support" }, - { - name: "--local-encoding=ENC", - description: "Use ENC as the local encoding for IRIs", - }, - { - name: "--remote-encoding=ENC", - description: "Use ENC as the default remote encoding", - }, - { name: "--unlink", description: "Remove file before clobber" }, - { - name: "--xattr", - description: "Turn on storage of metadata in extended file attributes", - }, - { - name: ["-nd", "--no-directories"], - description: "Don't create directories", - }, - { - name: ["-x", "--force-directories"], - description: "Force creation of directories", - }, - { - name: ["-nH", "--no-host-directories"], - description: "Don't create host directories", - }, - { - name: "--protocol-directories", - description: "Use protocol name in directories", - }, - { - name: ["-P", "--directory-prefix=PREFIX"], - description: "Save files to PREFIX/", - }, - { - name: "--cut-dirs=NUMBER", - description: "Ignore NUMBER remote directory components", - }, - { name: "--http-user=USER", description: "Set http user to USER" }, - { - name: "--http-password=PASS", - description: "Set http password to PASS", - }, - { name: "--no-cache", description: "Disallow server-cached data" }, - { - name: ["-E", "--adjust-extension"], - description: "Save HTML/CSS documents with proper extensions", - }, - { - name: "--ignore-length", - description: "Ignore 'Content-Length' header field", - }, - { - name: "--header=STRING", - description: "Insert STRING among the headers", - }, - { - name: "--compression=TYPE", - description: - "Choose compression, one of auto, gzip and none. (default: none)", - }, - { - name: "--max-redirect", - description: "Maximum redirections allowed per page", - }, - { name: "--proxy-user=USER", description: "Set USER as proxy username" }, - { - name: "--proxy-password=PASS", - description: "Set PASS as proxy password", - }, - { - name: "--referer=URL", - description: "Include 'Referer: URL' header in HTTP request", - }, - { name: "--save-headers", description: "Save the HTTP headers to file" }, - { - name: ["-U", "--user-agent=AGENT"], - description: "Identify as AGENT instead of Wget/VERSION", - }, - { - name: "--no-http-keep-alive", - description: "Disable HTTP keep-alive (persistent connections)", - }, - { name: "--no-cookies", description: "Don't use cookies" }, - { - name: "--load-cookies=FILE", - description: "Load cookies from FILE before session", - }, - { - name: "--save-cookies=FILE", - description: "Save cookies to FILE after session", - }, - { - name: "--keep-session-cookies", - description: "Load and save session (non-permanent) cookies", - }, - { - name: "--post-data=STRING", - description: "Use the POST method; send STRING as the data", - }, - { - name: "--post-file=FILE", - description: "Use the POST method; send contents of FILE", - }, - { - name: "--method=HTTPMethod", - description: 'Use method "HTTPMethod" in the request', - }, - { - name: "--body-data=STRING", - description: "Send STRING as data. --method MUST be set", - }, - { - name: "--body-file=FILE", - description: "Send contents of FILE. --method MUST be set", - }, - { - name: "--content-on-error", - description: "Output the received content on server errors", - }, - { - name: "--secure-protocol=PR", - description: "Choose secure protocol, one of auto, SSLv2,", - }, - { name: "--https-only", description: "Only follow secure HTTPS links" }, - { - name: "--no-check-certificate", - description: "Don't validate the server's certificate", - }, - { name: "--certificate=FILE", description: "Client certificate file" }, - { - name: "--certificate-type=TYPE", - description: "Client certificate type, PEM or DER", - }, - { name: "--private-key=FILE", description: "Private key file" }, - { - name: "--private-key-type=TYPE", - description: "Private key type, PEM or DER", - }, - { - name: "--ca-certificate=FILE", - description: "File with the bundle of CAs", - }, - { - name: "--ca-directory=DIR", - description: "Directory where hash list of CAs is stored", - }, - { name: "--crl-file=FILE", description: "File with bundle of CRLs" }, - { - name: "--ciphers=STR", - description: - "Set the priority string (GnuTLS) or cipher list string (OpenSSL) directly", - }, - { name: ["-r", "--recursive"], description: "Specify recursive download" }, - { - name: ["-l", "--level=NUMBER"], - description: "Maximum recursion depth (inf or 0 for infinite)", - }, - { - name: "--delete-after", - description: "Delete files locally after downloading them", - }, - { - name: ["-k", "--convert-links"], - description: "Make links in downloaded HTML or CSS point to local files", - }, - { - name: ["-K", "--backup-converted"], - description: "Before converting file X, back up as X.orig", - }, - { - name: ["-m", "--mirror"], - description: "Shortcut for -N -r -l inf --no-remove-listing", - }, - { - name: ["-p", "--page-requisites"], - description: "Get all images, etc. needed to display HTML page", - }, - { - name: ["-A", "--accept=LIST"], - description: "Comma-separated list of accepted extensions", - }, - { - name: ["-R", "--reject=LIST"], - description: "Comma-separated list of rejected extensions", - }, - { - name: "--accept-regex=REGEX", - description: "Regex matching accepted URLs", - }, - { - name: "--reject-regex=REGEX", - description: "Regex matching rejected URLs", - }, - { name: "--regex-type=TYPE", description: "Regex type (posix)" }, - { - name: ["-D", "--domains=LIST"], - description: "Comma-separated list of accepted domains", - }, - { - name: "--exclude-domains=LIST", - description: "Comma-separated list of rejected domains", - }, - { - name: "--follow-ftp", - description: "Follow FTP links from HTML documents", - }, - { - name: "--follow-tags=LIST", - description: "Comma-separated list of followed HTML tags", - }, - { - name: "--ignore-tags=LIST", - description: "Comma-separated list of ignored HTML tags", - }, - { - name: ["-H", "--span-hosts"], - description: "Go to foreign hosts when recursive", - }, - { name: ["-L", "--relative"], description: "Follow relative links only" }, - { - name: ["-I", "--include-directories=LIST"], - description: "List of allowed directories", - }, - { - name: ["-X", "--exclude-directories=LIST"], - description: "List of excluded directories", - }, - { - name: ["-np", "--no-parent"], - description: "Don't ascend to the parent directory", - }, - ], + name: "wget", + description: "A non-interactive network retriever", + args: { + isVariadic: true, + name: "url", + description: "The url(s) to retrieve", + }, + options: [ + { + name: ["-V", "--version"], + description: "Display the version of Wget and exit", + }, + { name: ["-h", "--help"], description: "Print this help" }, + { + name: ["-b", "--background"], + description: "Go to background after startup", + }, + { + name: ["-e", "--execute=COMMAND"], + description: "Execute a `.wgetrc'-style command", + }, + { name: ["-o", "--output-file=FILE"], description: "Log messages to FILE" }, + { + name: ["-a", "--append-output=FILE"], + description: "Append messages to FILE", + }, + { name: ["-q", "--quiet"], description: "Quiet (no output)" }, + { + name: ["-v", "--verbose"], + description: "Be verbose (this is the default)", + }, + { + name: ["-nv", "--no-verbose"], + description: "Turn off verboseness, without being quiet", + }, + { + name: "--report-speed=TYPE", + description: "Output bandwidth as TYPE. TYPE can be bits", + }, + { + name: ["-i", "--input-file=FILE"], + description: "Download URLs found in local or external FILE", + }, + { name: ["-F", "--force-html"], description: "Treat input file as HTML" }, + { + name: ["-B", "--base=URL"], + description: "Resolves HTML input-file links (-i -F) relative to URL", + }, + { name: "--config=FILE", description: "Specify config file to use" }, + { name: "--no-config", description: "Do not read any config file" }, + { + name: "--rejected-log=FILE", + description: "Log reasons for URL rejection to FILE", + }, + { + name: ["-t", "--tries=NUMBER"], + description: "Set number of retries to NUMBER (0 unlimits)", + }, + { + name: "--retry-connrefused", + description: "Retry even if connection is refused", + }, + { + name: "--retry-on-http-error", + description: "Comma-separated list of HTTP errors to retry", + }, + { + name: ["-O", "--output-document=FILE"], + description: "Write documents to FILE", + }, + { + name: ["-nc", "--no-clobber"], + description: + "Skip downloads that would download to existing files (overwriting them)", + }, + { + name: "--no-netrc", + description: "Don't try to obtain credentials from .netrc", + }, + { + name: ["-c", "--continue"], + description: "Resume getting a partially-downloaded file", + }, + { + name: "--start-pos=OFFSET", + description: "Start downloading from zero-based position OFFSET", + }, + { name: "--progress=TYPE", description: "Select progress gauge type" }, + { + name: "--show-progress", + description: "Display the progress bar in any verbosity mode", + }, + { + name: ["-N", "--timestamping"], + description: "Don't re-retrieve files unless newer than local", + }, + { name: ["-S", "--server-response"], description: "Print server response" }, + { name: "--spider", description: "Don't download anything" }, + { + name: ["-T", "--timeout=SECONDS"], + description: "Set all timeout values to SECONDS", + }, + { + name: "--dns-timeout=SECS", + description: "Set the DNS lookup timeout to SECS", + }, + { + name: "--connect-timeout=SECS", + description: "Set the connect timeout to SECS", + }, + { + name: "--read-timeout=SECS", + description: "Set the read timeout to SECS", + }, + { + name: ["-w", "--wait=SECONDS"], + description: "Wait SECONDS between retrievals", + }, + { + name: "--waitretry=SECONDS", + description: "Wait 1..SECONDS between retries of a retrieval", + }, + { + name: "--random-wait", + description: "Wait from 0.5*WAIT...1.5*WAIT secs between retrievals", + }, + { name: "--no-proxy", description: "Explicitly turn off proxy" }, + { + name: ["-Q", "--quota=NUMBER"], + description: "Set retrieval quota to NUMBER", + }, + { + name: "--bind-address=ADDRESS", + description: "Bind to ADDRESS (hostname or IP) on local host", + }, + { name: "--limit-rate=RATE", description: "Limit download rate to RATE" }, + { name: "--no-dns-cache", description: "Disable caching DNS lookups" }, + { + name: "--restrict-file-names=OS", + description: "Restrict chars in file names to ones OS allows", + }, + { + name: "--ignore-case", + description: "Ignore case when matching files/directories", + }, + { + name: ["-4", "--inet4-only"], + description: "Connect only to IPv4 addresses", + }, + { + name: ["-6", "--inet6-only"], + description: "Connect only to IPv6 addresses", + }, + { + name: "--user=USER", + description: "Set both ftp and http user to USER", + }, + { + name: "--password=PASS", + description: "Set both ftp and http password to PASS", + }, + { name: "--ask-password", description: "Prompt for passwords" }, + { name: "--no-iri", description: "Turn off IRI support" }, + { + name: "--local-encoding=ENC", + description: "Use ENC as the local encoding for IRIs", + }, + { + name: "--remote-encoding=ENC", + description: "Use ENC as the default remote encoding", + }, + { name: "--unlink", description: "Remove file before clobber" }, + { + name: "--xattr", + description: "Turn on storage of metadata in extended file attributes", + }, + { + name: ["-nd", "--no-directories"], + description: "Don't create directories", + }, + { + name: ["-x", "--force-directories"], + description: "Force creation of directories", + }, + { + name: ["-nH", "--no-host-directories"], + description: "Don't create host directories", + }, + { + name: "--protocol-directories", + description: "Use protocol name in directories", + }, + { + name: ["-P", "--directory-prefix=PREFIX"], + description: "Save files to PREFIX/", + }, + { + name: "--cut-dirs=NUMBER", + description: "Ignore NUMBER remote directory components", + }, + { name: "--http-user=USER", description: "Set http user to USER" }, + { + name: "--http-password=PASS", + description: "Set http password to PASS", + }, + { name: "--no-cache", description: "Disallow server-cached data" }, + { + name: ["-E", "--adjust-extension"], + description: "Save HTML/CSS documents with proper extensions", + }, + { + name: "--ignore-length", + description: "Ignore 'Content-Length' header field", + }, + { + name: "--header=STRING", + description: "Insert STRING among the headers", + }, + { + name: "--compression=TYPE", + description: + "Choose compression, one of auto, gzip and none. (default: none)", + }, + { + name: "--max-redirect", + description: "Maximum redirections allowed per page", + }, + { name: "--proxy-user=USER", description: "Set USER as proxy username" }, + { + name: "--proxy-password=PASS", + description: "Set PASS as proxy password", + }, + { + name: "--referer=URL", + description: "Include 'Referer: URL' header in HTTP request", + }, + { name: "--save-headers", description: "Save the HTTP headers to file" }, + { + name: ["-U", "--user-agent=AGENT"], + description: "Identify as AGENT instead of Wget/VERSION", + }, + { + name: "--no-http-keep-alive", + description: "Disable HTTP keep-alive (persistent connections)", + }, + { name: "--no-cookies", description: "Don't use cookies" }, + { + name: "--load-cookies=FILE", + description: "Load cookies from FILE before session", + }, + { + name: "--save-cookies=FILE", + description: "Save cookies to FILE after session", + }, + { + name: "--keep-session-cookies", + description: "Load and save session (non-permanent) cookies", + }, + { + name: "--post-data=STRING", + description: "Use the POST method; send STRING as the data", + }, + { + name: "--post-file=FILE", + description: "Use the POST method; send contents of FILE", + }, + { + name: "--method=HTTPMethod", + description: 'Use method "HTTPMethod" in the request', + }, + { + name: "--body-data=STRING", + description: "Send STRING as data. --method MUST be set", + }, + { + name: "--body-file=FILE", + description: "Send contents of FILE. --method MUST be set", + }, + { + name: "--content-on-error", + description: "Output the received content on server errors", + }, + { + name: "--secure-protocol=PR", + description: "Choose secure protocol, one of auto, SSLv2,", + }, + { name: "--https-only", description: "Only follow secure HTTPS links" }, + { + name: "--no-check-certificate", + description: "Don't validate the server's certificate", + }, + { name: "--certificate=FILE", description: "Client certificate file" }, + { + name: "--certificate-type=TYPE", + description: "Client certificate type, PEM or DER", + }, + { name: "--private-key=FILE", description: "Private key file" }, + { + name: "--private-key-type=TYPE", + description: "Private key type, PEM or DER", + }, + { + name: "--ca-certificate=FILE", + description: "File with the bundle of CAs", + }, + { + name: "--ca-directory=DIR", + description: "Directory where hash list of CAs is stored", + }, + { name: "--crl-file=FILE", description: "File with bundle of CRLs" }, + { + name: "--ciphers=STR", + description: + "Set the priority string (GnuTLS) or cipher list string (OpenSSL) directly", + }, + { name: ["-r", "--recursive"], description: "Specify recursive download" }, + { + name: ["-l", "--level=NUMBER"], + description: "Maximum recursion depth (inf or 0 for infinite)", + }, + { + name: "--delete-after", + description: "Delete files locally after downloading them", + }, + { + name: ["-k", "--convert-links"], + description: "Make links in downloaded HTML or CSS point to local files", + }, + { + name: ["-K", "--backup-converted"], + description: "Before converting file X, back up as X.orig", + }, + { + name: ["-m", "--mirror"], + description: "Shortcut for -N -r -l inf --no-remove-listing", + }, + { + name: ["-p", "--page-requisites"], + description: "Get all images, etc. needed to display HTML page", + }, + { + name: ["-A", "--accept=LIST"], + description: "Comma-separated list of accepted extensions", + }, + { + name: ["-R", "--reject=LIST"], + description: "Comma-separated list of rejected extensions", + }, + { + name: "--accept-regex=REGEX", + description: "Regex matching accepted URLs", + }, + { + name: "--reject-regex=REGEX", + description: "Regex matching rejected URLs", + }, + { name: "--regex-type=TYPE", description: "Regex type (posix)" }, + { + name: ["-D", "--domains=LIST"], + description: "Comma-separated list of accepted domains", + }, + { + name: "--exclude-domains=LIST", + description: "Comma-separated list of rejected domains", + }, + { + name: "--follow-ftp", + description: "Follow FTP links from HTML documents", + }, + { + name: "--follow-tags=LIST", + description: "Comma-separated list of followed HTML tags", + }, + { + name: "--ignore-tags=LIST", + description: "Comma-separated list of ignored HTML tags", + }, + { + name: ["-H", "--span-hosts"], + description: "Go to foreign hosts when recursive", + }, + { name: ["-L", "--relative"], description: "Follow relative links only" }, + { + name: ["-I", "--include-directories=LIST"], + description: "List of allowed directories", + }, + { + name: ["-X", "--exclude-directories=LIST"], + description: "List of excluded directories", + }, + { + name: ["-np", "--no-parent"], + description: "Don't ascend to the parent directory", + }, + ], }; // GNU Wget 1.20.3, a non-interactive network retriever. diff --git a/extensions/terminal-suggest/src/completions/upstream/yarn.ts b/extensions/terminal-suggest/src/completions/upstream/yarn.ts index a3de493a1b3..04c573a151b 100644 --- a/extensions/terminal-suggest/src/completions/upstream/yarn.ts +++ b/extensions/terminal-suggest/src/completions/upstream/yarn.ts @@ -82,7 +82,7 @@ const getGlobalPackagesGenerator: Fig.Generator = { name: dependencyName, icon: "📦", })); - } catch (e) { } + } catch (e) {} return []; }, @@ -101,7 +101,7 @@ const allDependenciesGenerator: Fig.Generator = { name: dependency.name.split("@")[0], icon: "📦", })); - } catch (e) { } + } catch (e) {} return []; }, }; @@ -127,7 +127,7 @@ const configList: Fig.Generator = { if (configObject) { return Object.keys(configObject).map((key) => ({ name: key })); } - } catch (e) { } + } catch (e) {} return []; }, @@ -367,7 +367,7 @@ export const createCLIsGenerator: Fig.Generator = { postProcess: function (out) { try { return JSON.parse(out).results.map( - (item: any) => + (item: { package: { name: string; description: string } }) => ({ name: item.package.name.substring(7), description: item.package.description, @@ -1550,9 +1550,9 @@ const completionSpec: Fig.Spec = { try { const workspacesDefinitions = isYarnV1 ? // transform Yarn V1 output to array of workspaces like Yarn V2 - await getWorkspacesDefinitionsV1() + await getWorkspacesDefinitionsV1() : // in yarn v>=2.0.0, workspaces definitions are a list of JSON lines - await getWorkspacesDefinitionsVOther(); + await getWorkspacesDefinitionsVOther(); const subcommands: Fig.Subcommand[] = workspacesDefinitions.map( ({ name, location }: { name: string; location: string }) => ({ @@ -1578,7 +1578,7 @@ const completionSpec: Fig.Spec = { name: script, })); } - } catch (e) { } + } catch (e) {} return []; }, }, diff --git a/extensions/terminal-suggest/src/constants.ts b/extensions/terminal-suggest/src/constants.ts index 91e0dfa107a..25ada5a06aa 100644 --- a/extensions/terminal-suggest/src/constants.ts +++ b/extensions/terminal-suggest/src/constants.ts @@ -46,7 +46,6 @@ export const upstreamSpecs = [ 'pnpm', 'node', 'nvm', - 'npx', ]; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts index c26f3a77143..3436414c4a6 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts @@ -35,6 +35,7 @@ import { } from './errors.js'; import { convertSubcommand, initializeDefault } from '../fig-autocomplete-shared'; import { exec, type ExecException } from 'child_process'; +import type { IFigExecuteExternals } from '../execute'; type ArgArrayState = { args: Array | null; @@ -605,8 +606,15 @@ const historyExecuteShellCommand: Fig.ExecuteCommandFunction = async () => { ); }; -const getExecuteShellCommandFunction = (isParsingHistory = false) => - isParsingHistory ? historyExecuteShellCommand : () => { throw new Error('Not implemented'); }; +function getExecuteShellCommandFunction( + isParsingHistory = false, + executeExternals: IFigExecuteExternals, +) { + if (isParsingHistory) { + return historyExecuteShellCommand; + } + return executeExternals.executeCommand; +} // const getGenerateSpecCacheKey = ( // completionObj: Internal.Subcommand, @@ -790,13 +798,14 @@ const parseArgumentsCached = async ( command: Command, context: Fig.ShellContext, spec: Fig.Spec, + executeExternals: IFigExecuteExternals, // authClient: AuthClient, isParsingHistory?: boolean, startIndex = 0, // localconsole: console.console = console, ): Promise => { // Route to cp.exec instead, we don't need to deal with ipc - const exec = getExecuteShellCommandFunction(isParsingHistory); + const exec = getExecuteShellCommandFunction(isParsingHistory, executeExternals); let currentCommand = command; let tokens = currentCommand.tokens.slice(startIndex); @@ -1124,6 +1133,7 @@ export const parseArguments = async ( command: Command | null, context: Fig.ShellContext, spec: Fig.Spec, + executeExternals: IFigExecuteExternals, // authClient: AuthClient, isParsingHistory = false, // localconsole: console.console = console, @@ -1157,6 +1167,7 @@ export const parseArguments = async ( context, // authClient, spec, + executeExternals, isParsingHistory, 0, ); diff --git a/extensions/terminal-suggest/src/fig/autocomplete/generators/scriptSuggestionsGenerator.ts b/extensions/terminal-suggest/src/fig/autocomplete/generators/scriptSuggestionsGenerator.ts index 07e90069d03..bd8463265c4 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete/generators/scriptSuggestionsGenerator.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete/generators/scriptSuggestionsGenerator.ts @@ -28,7 +28,7 @@ export async function getScriptSuggestions( } try { - const { isDangerous, tokenArray, currentWorkingDirectory } = context; + const { isDangerous, tokenArray, currentWorkingDirectory, environmentVariables } = context; // A script can either be a string or a function that returns a string. // If the script is a function, run it, and get the output string. const commandToRun = @@ -46,6 +46,7 @@ export async function getScriptSuggestions( command: commandToRun[0], args: commandToRun.slice(1), cwd: currentWorkingDirectory, + env: environmentVariables }; } else { executeCommandInput = { diff --git a/extensions/terminal-suggest/src/fig/execute.ts b/extensions/terminal-suggest/src/fig/execute.ts index ab9792f6137..2c642019b67 100644 --- a/extensions/terminal-suggest/src/fig/execute.ts +++ b/extensions/terminal-suggest/src/fig/execute.ts @@ -20,7 +20,7 @@ export const executeCommandTimeout = async ( ): Promise => { const command = [input.command, ...input.args].join(' '); try { - console.info(`About to run shell command '${command}'`); + console.debug(`About to run shell command '${command}'`); const result = await withTimeout( Math.max(timeout, input.timeout ?? 0), spawnHelper2(input.command, input.args, { diff --git a/extensions/terminal-suggest/src/fig/figInterface.ts b/extensions/terminal-suggest/src/fig/figInterface.ts index 1e283e46355..8d5a139ea90 100644 --- a/extensions/terminal-suggest/src/fig/figInterface.ts +++ b/extensions/terminal-suggest/src/fig/figInterface.ts @@ -16,7 +16,7 @@ import type { ICompletionResource } from '../types'; import { osIsWindows } from '../helpers/os'; import { removeAnyFileExtension } from '../helpers/file'; import type { EnvironmentVariable } from './api-bindings/types'; -import { asArray } from '../terminalSuggestMain'; +import { asArray, availableSpecs } from '../terminalSuggestMain'; import { IFigExecuteExternals } from './execute'; export interface IFigSpecSuggestionsResult { @@ -52,11 +52,10 @@ export async function getFigSuggestions( if (!specLabels) { continue; } - for (const specLabel of specLabels) { const availableCommand = (osIsWindows() ? availableCommands.find(command => (typeof command.label === 'string' ? command.label : command.label.label).match(new RegExp(`${specLabel}(\\.[^ ]+)?$`))) - : availableCommands.find(command => (typeof command.label === 'string' ? command.label : command.label.label).startsWith(specLabel))); + : availableCommands.find(command => (typeof command.label === 'string' ? command.label : command.label.label) === (specLabel))); if (!availableCommand || (token && token.isCancellationRequested)) { continue; } @@ -81,17 +80,20 @@ export async function getFigSuggestions( const commandAndAliases = (osIsWindows() ? availableCommands.filter(command => specLabel === removeAnyFileExtension(command.definitionCommand ?? (typeof command.label === 'string' ? command.label : command.label.label))) - : availableCommands.filter(command => specLabel === (command.definitionCommand ?? command.label))); + : availableCommands.filter(command => specLabel === (command.definitionCommand ?? (typeof command.label === 'string' ? command.label : command.label.label)))); if ( !(osIsWindows() ? commandAndAliases.some(e => precedingText.startsWith(`${removeAnyFileExtension((typeof e.label === 'string' ? e.label : e.label.label))} `)) - : commandAndAliases.some(e => precedingText.startsWith(`${e.label} `))) + : commandAndAliases.some(e => precedingText.startsWith(`${typeof e.label === 'string' ? e.label : e.label.label} `))) ) { - // the spec label is not the first word in the command line, so do not provide options or args continue; } - const completionItemResult = await getFigSpecSuggestions(spec, terminalContext, prefix, shellIntegrationCwd, env, name, executeExternals, token); + const actualSpec = availableCommand.definitionCommand ? availableSpecs.find(s => s.name === availableCommand.definitionCommand) : spec; + if (!actualSpec) { + continue; + } + const completionItemResult = await getFigSpecSuggestions(actualSpec, terminalContext, prefix, shellIntegrationCwd, env, name, executeExternals, token); result.hasCurrentArg ||= !!completionItemResult?.hasCurrentArg; if (completionItemResult) { result.filesRequested ||= completionItemResult.filesRequested; @@ -131,7 +133,7 @@ async function getFigSpecSuggestions( currentProcess: name, // TODO: pass in aliases }; - const parsedArguments: ArgumentParserResult = await parseArguments(command, shellContext, spec); + const parsedArguments: ArgumentParserResult = await parseArguments(command, shellContext, spec, executeExternals); const items: vscode.TerminalCompletionItem[] = []; // TODO: Pass in and respect cancellation token @@ -263,11 +265,13 @@ export async function collectCompletionItemResult( } let itemKind = kind; - if (typeof item === 'object' && 'args' in item && (asArray(item.args ?? [])).length > 0) { - itemKind = vscode.TerminalCompletionItemKind.Option; - } const lastArgType: string | undefined = parsedArguments?.annotations.at(-1)?.type; - if (lastArgType === 'option_arg') { + if (lastArgType === 'subcommand_arg') { + if (typeof item === 'object' && 'args' in item && (asArray(item.args ?? [])).length > 0) { + itemKind = vscode.TerminalCompletionItemKind.Option; + } + } + else if (lastArgType === 'option_arg') { itemKind = vscode.TerminalCompletionItemKind.OptionValue; } diff --git a/extensions/terminal-suggest/src/helpers/promise.ts b/extensions/terminal-suggest/src/helpers/promise.ts new file mode 100644 index 00000000000..14e86bc0481 --- /dev/null +++ b/extensions/terminal-suggest/src/helpers/promise.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function createTimeoutPromise(timeout: number, defaultValue: T): Promise { + return new Promise(resolve => setTimeout(() => resolve(defaultValue), timeout)); +} diff --git a/extensions/terminal-suggest/src/shell/bash.ts b/extensions/terminal-suggest/src/shell/bash.ts index 8db683af892..15744908971 100644 --- a/extensions/terminal-suggest/src/shell/bash.ts +++ b/extensions/terminal-suggest/src/shell/bash.ts @@ -16,7 +16,8 @@ export async function getBashGlobals(options: ExecOptionsWithStringEncoding, exi } async function getAliases(options: ExecOptionsWithStringEncoding): Promise { - return getAliasesHelper('bash', ['-ic', 'alias'], /^alias (?[a-zA-Z0-9\.:-]+)='(?.+)'$/, options); + const args = process.platform === 'darwin' ? ['-icl', 'alias'] : ['-ic', 'alias']; + return getAliasesHelper('bash', args, /^alias (?[a-zA-Z0-9\.:-]+)='(?.+)'$/, options); } export async function getBuiltins( diff --git a/extensions/terminal-suggest/src/shell/fish.ts b/extensions/terminal-suggest/src/shell/fish.ts index cc2fbaff6e0..98c1ac09556 100644 --- a/extensions/terminal-suggest/src/shell/fish.ts +++ b/extensions/terminal-suggest/src/shell/fish.ts @@ -3,21 +3,87 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; import type { ICompletionResource } from '../types'; -import { execHelper, getAliasesHelper } from './common'; +import { getAliasesHelper } from './common'; import { type ExecOptionsWithStringEncoding } from 'node:child_process'; +import { fishBuiltinsCommandDescriptionsCache } from './fishBuiltinsCache'; + +const commandDescriptionsCache: Map | undefined = parseCache(fishBuiltinsCommandDescriptionsCache); export async function getFishGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { return [ ...await getAliases(options), - ...await getBuiltins(options, existingCommands), + ...await getBuiltins(options), ]; } -async function getBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { - const compgenOutput = await execHelper('functions -n', options); - const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); - return compgenOutput.split(', ').filter(filter); +async function getBuiltins(options: ExecOptionsWithStringEncoding): Promise<(string | ICompletionResource)[]> { + const completions: ICompletionResource[] = []; + + // Use the cache directly for all commands + for (const cmd of [...commandDescriptionsCache!.keys()]) { + try { + const result = getCommandDescription(cmd); + if (result) { + completions.push({ + label: { label: cmd, description: result.description }, + detail: result.args, + documentation: new vscode.MarkdownString(result.documentation), + kind: vscode.TerminalCompletionItemKind.Method + }); + } else { + console.warn(`Fish command "${cmd}" not found in cache.`); + completions.push({ + label: cmd, + kind: vscode.TerminalCompletionItemKind.Method + }); + } + } catch (e) { + // Ignore errors + completions.push({ + label: cmd, + kind: vscode.TerminalCompletionItemKind.Method + }); + } + } + + return completions; +} + +export function getCommandDescription(command: string): { documentation?: string; description?: string; args?: string | undefined } | undefined { + if (!commandDescriptionsCache) { + return undefined; + } + const result = commandDescriptionsCache.get(command); + if (!result) { + return undefined; + } + + if (result.shortDescription) { + return { + description: result.shortDescription, + args: result.args, + documentation: result.description + }; + } else { + return { + description: result.description, + args: result.args, + documentation: result.description + }; + } +} + +function parseCache(cache: Object): Map | undefined { + if (!cache) { + return undefined; + } + const result = new Map(); + for (const [key, value] of Object.entries(cache)) { + result.set(key, value); + } + return result; } async function getAliases(options: ExecOptionsWithStringEncoding): Promise { diff --git a/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts b/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts new file mode 100644 index 00000000000..2a5aefc4aa6 --- /dev/null +++ b/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts @@ -0,0 +1,301 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const fishBuiltinsCommandDescriptionsCache = { + ".": { + "shortDescription": "source - evaluate contents of file", + "description": "source evaluates the commands of the specified FILE in the current\nshell as a new block of code. This is different from starting a new\nprocess to perform the commands (i.e. fish < FILE) since the commands\nwill be evaluated by the current shell, which means that changes in\nshell variables will affect the current shell. If additional\narguments are specified after the file name, they will be inserted\ninto the argv variable. The argv variable will not include the name\nof the sourced file.\n\nfish will search the working directory to resolve relative paths but\nwill not search PATH .\n\nIf no file is specified and stdin is not the terminal, or if the file\nname - is used, stdin will be read.\n\nThe exit status of source is the exit status of the last job to\nexecute. If something goes wrong while opening or reading the file,\nsource exits with a non-zero status.\n\n. (a single period) is an alias for the source command. The use of .\nis deprecated in favour of source, and . will be removed in a future\nversion of fish.\n\nsource creates a new local scope; set --local within a sourced block\nwill not affect variables in the enclosing scope.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\n\n source ~/.config/fish/config.fish\n # Causes fish to re-read its initialization file.\n\nCAVEATS\nIn fish versions prior to 2.3.0, the argv variable would have a\nsingle element (the name of the sourced file) if no arguments are\npresent. Otherwise, it would contain arguments without the name of\nthe sourced file. That behavior was very confusing and unlike other\nshells such as bash and zsh.", + "args": "source FILE [ARGUMENTS ...]\nSOMECOMMAND | source" + }, + ":": { + "shortDescription": "No operation command", + "description": "The `:` command is a no-op (no operation) command that returns a successful (zero) exit status. It can be used as a placeholder in scripts where a command is syntactically required but no action is desired." + }, + "[": { + "shortDescription": "Test if a statement is true", + "description": "Evaluate an expression and return a status of true (0) or false (non-zero). Unlike the `test` command, the `[` command requires a closing `]`.", + "args": "EXPRESSION ]" + }, + "_": { + "shortDescription": "", + "description": "" + }, + "abbr": { + "shortDescription": "manage fish abbreviations", + "description": "abbr manages abbreviations - user-defined words that are replaced\nwith longer phrases when entered.\n\nNOTE:\n Only typed-in commands use abbreviations. Abbreviations are not\n expanded in scripts.\n\nFor example, a frequently-run command like git checkout can be\nabbreviated to gco. After entering gco and pressing Space or Enter,\nthe full text git checkout will appear in the command line. To avoid\nexpanding something that looks like an abbreviation, the default\nControl+Space binding inserts a space without expanding.\n\nAn abbreviation may match a literal word, or it may match a pattern\ngiven by a regular expression. When an abbreviation matches a word,\nthat word is replaced by new text, called its expansion. This\nexpansion may be a fixed new phrase, or it can be dynamically created\nvia a fish function. This expansion occurs after pressing space or\nenter.\n\nCombining these features, it is possible to create custom syntaxes,\nwhere a regular expression recognizes matching tokens, and the\nexpansion function interprets them. See the Examples section.\n\nChanged in version 3.6.0: Previous versions of this allowed saving\nabbreviations in universal variables. That's no longer possible.\nExisting variables will still be imported and abbr --erase will also\nerase the variables. We recommend adding abbreviations to\nconfig.fish by just adding the abbr --add command. When you run\nabbr, you will see output like this\n\n > abbr\n abbr -a -- foo bar # imported from a universal variable, see `help abbr`\n\nIn that case you should take the part before the # comment and save\nit in config.fish, then you can run abbr --erase to remove the\nuniversal variable:\n\n > abbr >> ~/.config/fish/config.fish\n > abbr --erase (abbr --list)\n\nADD SUBCOMMAND\nabbr [-a | --add] NAME [--position command | anywhere] [-r | --regex PATTERN]\n [--set-cursor[=MARKER]] ([-f | --function FUNCTION] | EXPANSION)\n\nabbr --add creates a new abbreviation. With no other options, the\nstring NAME is replaced by EXPANSION.\n\nWith --position command, the abbreviation will only expand when it is\npositioned as a command, not as an argument to another command. With\n--position anywhere the abbreviation may expand anywhere in the\ncommand line. The default is command.\n\nWith --regex, the abbreviation matches using the regular expression\ngiven by PATTERN, instead of the literal NAME. The pattern is\ninterpreted using PCRE2 syntax and must match the entire token. If\nmultiple abbreviations match the same token, the last abbreviation\nadded is used.\n\nWith --set-cursor=MARKER, the cursor is moved to the first occurrence\nof MARKER in the expansion. The MARKER value is erased. The MARKER\nmay be omitted (i.e. simply --set-cursor), in which case it defaults\nto %.\n\nWith -f FUNCTION or --function FUNCTION, FUNCTION is treated as the\nname of a fish function instead of a literal replacement. When the\nabbreviation matches, the function will be called with the matching\ntoken as an argument. If the function's exit status is 0 (success),\nthe token will be replaced by the function's output; otherwise the\ntoken will be left unchanged. No EXPANSION may be given separately.\n\n Examples\n\n abbr --add gco git checkout\n\nAdd a new abbreviation where gco will be replaced with git checkout.\n\n abbr -a --position anywhere -- -C --color\n\nAdd a new abbreviation where -C will be replaced with --color. The --\nallows -C to be treated as the name of the abbreviation, instead of\nan option.\n\n abbr -a L --position anywhere --set-cursor \"% | less\"\n\nAdd a new abbreviation where L will be replaced with | less, placing\nthe cursor before the pipe.\n\n function last_history_item\n echo $history[1]\n end\n abbr -a !! --position anywhere --function last_history_item\n\nThis first creates a function last_history_item which outputs the\nlast entered command. It then adds an abbreviation which replaces !!\nwith the result of calling this function. Taken together, this is\nsimilar to the !! history expansion feature of bash.\n\n function vim_edit\n echo vim $argv\n end\n abbr -a vim_edit_texts --position command --regex \".+\\.txt\" --function vim_edit\n\nThis first creates a function vim_edit which prepends vim before its\nargument. It then adds an abbreviation which matches commands ending\nin .txt, and replaces the command with the result of calling this\nfunction. This allows text files to be \"executed\" as a command to\nopen them in vim, similar to the \"suffix alias\" feature in zsh.\n\n abbr 4DIRS --set-cursor=! \"$(string join \\n -- 'for dir in */' 'cd $dir' '!' 'cd ..' 'end')\"\n\nThis creates an abbreviation \"4DIRS\" which expands to a multi-line\nloop \"template.\" The template enters each directory and then leaves\nit. The cursor is positioned ready to enter the command to run in\neach directory, at the location of the !, which is itself erased.\n\nOTHER SUBCOMMANDS\n\n abbr --rename OLD_NAME NEW_NAME\n\nRenames an abbreviation, from OLD_NAME to NEW_NAME\n\n abbr [-s | --show]\n\nShow all abbreviations in a manner suitable for import and export\n\n abbr [-l | --list]\n\nPrints the names of all abbreviation\n\n abbr [-e | --erase] NAME\n\nErases the abbreviation with the given name\n\n abbr -q or --query [NAME...]\n\nReturn 0 (true) if one of the NAME is an abbreviation.\n\n abbr -h or --help\n\nDisplays help for the abbr command.", + "args": "abbr --add NAME [--position command | anywhere] [-r | --regex PATTERN]\n[--set-cursor[=MARKER]] ([-f | --function FUNCTION] | EXPANSION)\nabbr --erase NAME ...\nabbr --rename OLD_WORD NEW_WORD\nabbr --show\nabbr --list\nabbr --query NAME ..." + }, + "and": { + "shortDescription": "conditionally execute a command", + "description": "and is used to execute a command if the previous command was\nsuccessful (returned a status of 0).\n\nand statements may be used as part of the condition in an while or if\nblock.\n\nand does not change the current exit status itself, but the command\nit runs most likely will. The exit status of the last foreground\ncommand to exit can always be accessed using the $status variable.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nThe following code runs the make command to build a program. If the\nbuild succeeds, make's exit status is 0, and the program is\ninstalled. If either step fails, the exit status is 1, and make clean\nis run, which removes the files created by the build process.\n\n make; and make install; or make clean\n\nSEE ALSO\n\n• or command\n\n• not command", + "args": "PREVIOUS; and COMMAND" + }, + "argparse": { + "shortDescription": "parse options passed to a fish script or function", + "description": "This command makes it easy for fish scripts and functions to handle\narguments. You pass arguments that define the known options, followed\nby a literal --, then the arguments to be parsed (which might also\ninclude a literal --). argparse then sets variables to indicate the\npassed options with their values, and sets $argv to the remaining\narguments. See the usage section below.\n\nEach option specification (OPTION_SPEC) is written in the domain\nspecific language described below. All OPTION_SPECs must appear after\nany argparse flags and before the -- that separates them from the\narguments to be parsed.\n\nEach option that is seen in the ARG list will result in variables\nnamed _flag_X, where X is the short flag letter and the long flag\nname (if they are defined). For example a --help option could cause\nargparse to define one variable called _flag_h and another called\n_flag_help.\n\nThe variables will be set with local scope (i.e., as if the script\nhad done set -l _flag_X). If the flag is a boolean (that is, it just\nis passed or not, it doesn't have a value) the values are the short\nand long flags seen. If the option is not a boolean the values will\nbe zero or more values corresponding to the values collected when the\nARG list is processed. If the flag was not seen the flag variable\nwill not be set.\n\nOPTIONS\nThe following argparse options are available. They must appear before\nall OPTION_SPECs:\n\n-n or --name\n The command name for use in error messages. By default the\n current function name will be used, or argparse if run outside\n of a function.\n\n-x or --exclusive OPTIONS\n A comma separated list of options that are mutually exclusive.\n You can use this more than once to define multiple sets of\n mutually exclusive options. You give either the short or long\n version of each option, and you still need to otherwise define\n the options.\n\n-N or --min-args NUMBER\n The minimum number of acceptable non-option arguments. The\n default is zero.\n\n-X or --max-args NUMBER\n The maximum number of acceptable non-option arguments. The\n default is infinity.\n\n-i or --ignore-unknown\n Ignores unknown options, keeping them and their arguments in\n $argv instead.\n\n-s or --stop-nonopt\n Causes scanning the arguments to stop as soon as the first\n non-option argument is seen. Among other things, this is\n useful to implement subcommands that have their own options.\n\n-h or --help\n Displays help about using this command.\n\nUSAGE\nTo use this command, pass the option specifications (OPTION_SPEC), a\nmandatory --, and then the arguments to be parsed.\n\nA simple example:\n\n argparse --name=my_function 'h/help' 'n/name=' -- $argv\n or return\n\nIf $argv is empty then there is nothing to parse and argparse returns\nzero to indicate success. If $argv is not empty then it is checked\nfor flags -h, --help, -n and --name. If they are found they are\nremoved from the arguments and local variables called _flag_OPTION\nare set so the script can determine which options were seen. If $argv\ndoesn't have any errors, like a missing mandatory value for an\noption, then argparse exits with a status of zero. Otherwise it\nwrites appropriate error messages to stderr and exits with a status\nof one.\n\nThe or return means that the function returns argparse's status if it\nfailed, so if it goes on argparse succeeded.\n\nThe -- argument is required. You do not have to include any option\nspecifications or arguments after the -- but you must include the --.\nFor example, this is acceptable:\n\n set -l argv foo\n argparse 'h/help' 'n/name' -- $argv\n argparse --min-args=1 -- $argv\n\nBut this is not:\n\n set -l argv\n argparse 'h/help' 'n/name' $argv\n\nThe first -- seen is what allows the argparse command to reliably\nseparate the option specifications and options to argparse itself\n(like --ignore-unknown) from the command arguments, so it is\nrequired.\n\nOPTION SPECIFICATIONS\nEach option specification consists of:\n\n• An optional alphanumeric short flag character, followed by a / if\n the short flag can be used by someone invoking your command or, for\n backwards compatibility, a - if it should not be exposed as a valid\n short flag (in which case it will also not be exposed as a flag\n variable).\n\n• An optional long flag name, which if not present the short flag can\n be used, and if that is also not present, an error is reported\n\n• Nothing if the flag is a boolean that takes no argument or is an\n integer flag, or\n\n • = if it requires a value and only the last instance of the\n flag is saved, or\n\n • =? if it takes an optional value and only the last instance of\n the flag is saved, or\n\n • =+ if it requires a value and each instance of the flag is\n saved.\n\n• Optionally a ! followed by fish script to validate the value.\n Typically this will be a function to run. If the exit status is\n zero the value for the flag is valid. If non-zero the value is\n invalid. Any error messages should be written to stdout (not\n stderr). See the section on Flag Value Validation for more\n information.\n\nSee the fish_opt command for a friendlier but more verbose way to\ncreate option specifications.\n\nIf a flag is not seen when parsing the arguments then the\ncorresponding _flag_X var(s) will not be set.\n\nINTEGER FLAG\nSometimes commands take numbers directly as options, like foo -55. To\nallow this one option spec can have the # modifier so that any\ninteger will be understood as this flag, and the last number will be\ngiven as its value (as if = was used).\n\nThe # must follow the short flag letter (if any), and other modifiers\nlike = are not allowed, except for - (for backwards compatibility):\n\n m#maximum\n\nThis does not read numbers given as +NNN, only those that look like\nflags - -NNN.\n\nNOTE: OPTIONAL ARGUMENTS\nAn option defined with =? can take optional arguments. Optional\narguments have to be directly attached to the option they belong to.\n\nThat means the argument will only be used for the option if you use\nit like:\n\n cmd --flag=value\n # or\n cmd -fvalue\n\nbut not if used like:\n\n cmd --flag value\n # \"value\" here will be used as a positional argument\n # and \"--flag\" won't have an argument.\n\nIf this weren't the case, using an option without an optional\nargument would be difficult if you also wanted to use positional\narguments.\n\nFor example:\n\n grep --color auto\n # Here \"auto\" will be used as the search string,\n # \"color\" will not have an argument and will fall back to the default,\n # which also *happens to be* auto.\n grep --color always\n # Here grep will still only use color \"auto\"matically\n # and search for the string \"always\".\n\nThis isn't specific to argparse but common to all things using\ngetopt(3) (if they have optional arguments at all). That grep example\nis how GNU grep actually behaves.\n\nFLAG VALUE VALIDATION\nSometimes you need to validate the option values. For example, that\nit is a valid integer within a specific range, or an ip address, or\nsomething entirely different. You can always do this after argparse\nreturns but you can also request that argparse perform the validation\nby executing arbitrary fish script. To do so simply append an !\n(exclamation-mark) then the fish script to be run. When that code is\nexecuted three vars will be defined:\n\n• _argparse_cmd will be set to the value of the value of the argparse\n --name value.\n\n• _flag_name will be set to the short or long flag that being\n processed.\n\n• _flag_value will be set to the value associated with the flag being\n processed.\n\nThese variables are passed to the function as local exported\nvariables.\n\nThe script should write any error messages to stdout, not stderr. It\nshould return a status of zero if the flag value is valid otherwise a\nnon-zero status to indicate it is invalid.\n\nFish ships with a _validate_int function that accepts a --min and\n--max flag. Let's say your command accepts a -m or --max flag and the\nminimum allowable value is zero and the maximum is 5. You would\ndefine the option like this: m/max=!_validate_int --min 0 --max 5.\nThe default if you just call _validate_int without those flags is to\nsimply check that the value is a valid integer with no limits on the\nmin or max value allowed.\n\nHere are some examples of flag validations:\n\n # validate that a path is a directory\n argparse 'p/path=!test -d \"$_flag_value\"' -- --path $__fish_config_dir\n # validate that a function does not exist\n argparse 'f/func=!not functions -q \"$_flag_value\"' -- -f alias\n # validate that a string matches a regex\n argparse 'c/color=!string match -rq \\'^#?[0-9a-fA-F]{6}$\\' \"$_flag_value\"' -- -c 'c0ffee'\n # validate with a validator function\n argparse 'n/num=!_validate_int --min 0 --max 99' -- --num 42\n\nEXAMPLE OPTION_SPECS\nSome OPTION_SPEC examples:\n\n• h/help means that both -h and --help are valid. The flag is a\n boolean and can be used more than once. If either flag is used then\n _flag_h and _flag_help will be set to however either flag was seen,\n as many times as it was seen. So it could be set to -h, -h and\n --help, and count $_flag_h would yield \"3\".\n\n• help means that only --help is valid. The flag is a boolean and can\n be used more than once. If it is used then _flag_help will be set\n as above. Also h-help (with an arbitrary short letter) for\n backwards compatibility.\n\n• longonly= is a flag --longonly that requires an option, there is no\n short flag or even short flag variable.\n\n• n/name= means that both -n and --name are valid. It requires a\n value and can be used at most once. If the flag is seen then\n _flag_n and _flag_name will be set with the single mandatory value\n associated with the flag.\n\n• n/name=? means that both -n and --name are valid. It accepts an\n optional value and can be used at most once. If the flag is seen\n then _flag_n and _flag_name will be set with the value associated\n with the flag if one was provided else it will be set with no\n values.\n\n• name=+ means that only --name is valid. It requires a value and can\n be used more than once. If the flag is seen then _flag_name will be\n set with the values associated with each occurrence.\n\n• x means that only -x is valid. It is a boolean that can be used\n more than once. If it is seen then _flag_x will be set as above.\n\n• x=, x=?, and x=+ are similar to the n/name examples above but there\n is no long flag alternative to the short flag -x.\n\n• #max (or #-max) means that flags matching the regex \"^--?\\d+$\" are\n valid. When seen they are assigned to the variable _flag_max. This\n allows any valid positive or negative integer to be specified by\n prefixing it with a single \"-\". Many commands support this idiom.\n For example head -3 /a/file to emit only the first three lines of\n /a/file.\n\n• n#max means that flags matching the regex \"^--?\\d+$\" are valid.\n When seen they are assigned to the variables _flag_n and _flag_max.\n This allows any valid positive or negative integer to be specified\n by prefixing it with a single \"-\". Many commands support this\n idiom. For example head -3 /a/file to emit only the first three\n lines of /a/file. You can also specify the value using either flag:\n -n NNN or --max NNN in this example.\n\n• #longonly causes the last integer option to be stored in\n _flag_longonly.\n\nAfter parsing the arguments the argv variable is set with local scope\nto any values not already consumed during flag processing. If there\nare no unbound values the variable is set but count $argv will be\nzero.\n\nIf an error occurs during argparse processing it will exit with a\nnon-zero status and print error messages to stderr.\n\nEXAMPLES\nA simple use:\n\n argparse h/help -- $argv\n or return\n\n if set -q _flag_help\n # TODO: Print help here\n return 0\n end\n\nThis just wants one option - -h / --help. Any other option is an\nerror. If it is given it prints help and exits.\n\nHow fish_add_path - add to the path parses its args:\n\n argparse -x g,U -x P,U -x a,p g/global U/universal P/path p/prepend a/append h/help m/move v/verbose n/dry-run -- $argv\n\nThere are a variety of boolean flags, all with long and short\nversions. A few of these cannot be used together, and that is what\nthe -x flag is used for. -x g,U means that --global and --universal\nor their short equivalents conflict, and if they are used together\nyou get an error. In this case you only need to give the short or\nlong flag, not the full option specification.\n\nAfter this it figures out which variable it should operate on\naccording to the --path flag:\n\n set -l var fish_user_paths\n set -q _flag_path\n and set var PATH\n\nLIMITATIONS\nOne limitation with --ignore-unknown is that, if an unknown option is\ngiven in a group with known options, the entire group will be kept in\n$argv. argparse will not do any permutations here.\n\nFor instance:\n\n argparse --ignore-unknown h -- -ho\n echo $_flag_h # is -h, because -h was given\n echo $argv # is still -ho\n\nThis limitation may be lifted in future.\n\nAdditionally, it can only parse known options up to the first unknown\noption in the group - the unknown option could take options, so it\nisn't clear what any character after an unknown option means.", + "args": "argparse [OPTIONS] OPTION_SPEC ... -- [ARG ...]" + }, + "begin": { + "shortDescription": "start a new block of code", + "description": "begin is used to create a new block of code.\n\nA block allows the introduction of a new variable scope, redirection\nof the input or output of a set of commands as a group, or to specify\nprecedence when using the conditional commands like and.\n\nThe block is unconditionally executed. begin; ...; end is equivalent\nto if true; ...; end.\n\nbegin does not change the current exit status itself. After the block\nhas completed, $status will be set to the status returned by the most\nrecent command.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nThe following code sets a number of variables inside of a block\nscope. Since the variables are set inside the block and have local\nscope, they will be automatically deleted when the block ends.\n\n begin\n set -l PIRATE Yarrr\n\n ...\n end\n\n echo $PIRATE\n # This will not output anything, since the PIRATE variable\n # went out of scope at the end of the block\n\nIn the following code, all output is redirected to the file out.html.\n\n begin\n echo $xml_header\n echo $html_header\n if test -e $file\n ...\n end\n ...\n end > out.html", + "args": "begin; [COMMANDS ...]; end" + }, + "bg": { + "shortDescription": "send jobs to background", + "description": "bg sends jobs to the background, resuming them if they are stopped.\n\nA background job is executed simultaneously with fish, and does not\nhave access to the keyboard. If no job is specified, the last job to\nbe used is put in the background. If PID is specified, the jobs\ncontaining the specified process IDs are put in the background.\n\nFor compatibility with other shells, job expansion syntax is\nsupported for bg. A PID of the format %1 will be interpreted as the\nPID of job 1. Job numbers can be seen in the output of jobs.\n\nWhen at least one of the arguments isn't a valid job specifier, bg\nwill print an error without backgrounding anything.\n\nWhen all arguments are valid job specifiers, bg will background all\nmatching jobs that exist.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nbg 123 456 789 will background the jobs that contain processes 123,\n456 and 789.\n\nIf only 123 and 789 exist, it will still background them and print an\nerror about 456.\n\nbg 123 banana or bg banana 123 will complain that \"banana\" is not a\nvalid job specifier.\n\nbg %1 will background job 1.", + "args": "bg [PID ...]" + }, + "bind": { + "shortDescription": "handle fish key bindings", + "description": "bind manages bindings.\n\nIt can add bindings if given a SEQUENCE of characters to bind to.\nThese should be written as fish escape sequences. The most important\nof these are \\c for the control key, and \\e for escape, and because\nof historical reasons also the Alt key (sometimes also called\n\"Meta\").\n\nFor example, Alt+W can be written as \\ew, and Control+X (^X) can be\nwritten as \\cx. Note that Alt-based key bindings are case sensitive\nand Control-based key bindings are not. This is a constraint of\ntext-based terminals, not fish.\n\nThe generic key binding that matches if no other binding does can be\nset by specifying a SEQUENCE of the empty string (that is, '' ). For\nmost key bindings, it makes sense to bind this to the self-insert\nfunction (i.e. bind '' self-insert). This will insert any keystrokes\nnot specifically bound to into the editor. Non-printable characters\nare ignored by the editor, so this will not result in control\nsequences being inserted.\n\nIf the -k switch is used, the name of a key (such as 'down', 'up' or\n'backspace') is used instead of a sequence. The names used are the\nsame as the corresponding curses variables, but without the 'key'\nprefix. (See terminfo(5) for more information, or use bind\n--key-names for a list of all available named keys). Normally this\nwill print an error if the current $TERM entry doesn't have a given\nkey, unless the -s switch is given.\n\nTo find out what sequence a key combination sends, you can use\nfish_key_reader.\n\nCOMMAND can be any fish command, but it can also be one of a set of\nspecial input functions. These include functions for moving the\ncursor, operating on the kill-ring, performing tab completion, etc.\nUse bind --function-names for a complete list of these input\nfunctions.\n\nWhen COMMAND is a shellscript command, it is a good practice to put\nthe actual code into a function and simply bind to the function name.\nThis way it becomes significantly easier to test the function while\nediting, and the result is usually more readable as well.\n\nNOTE:\n Special input functions cannot be combined with ordinary shell\n script commands. The commands must be entirely a sequence of\n special input functions (from bind -f) or all shell script\n commands (i.e., valid fish script). To run special input functions\n from regular fish script, use commandline -f (see also\n commandline). If a script produces output, it should finish by\n calling commandline -f repaint to tell fish that a repaint is in\n order.\n\nIf no SEQUENCE is provided, all bindings (or just the bindings in the\ngiven MODE) are printed. If SEQUENCE is provided but no COMMAND, just\nthe binding matching that sequence is printed.\n\nTo save custom key bindings, put the bind statements into\nconfig.fish. Alternatively, fish also automatically executes a\nfunction called fish_user_key_bindings if it exists.\n\nKey bindings may use \"modes\", which mimics Vi's modal input behavior.\nThe default mode is \"default\". Every key binding applies to a single\nmode; you can specify which one with -M MODE. If the key binding\nshould change the mode, you can specify the new mode with -m\nNEW_MODE. The mode can be viewed and changed via the $fish_bind_mode\nvariable. If you want to change the mode from inside a fish function,\nuse set fish_bind_mode MODE.\n\nOPTIONS\nThe following options are available:\n\n-k or --key\n Specify a key name, such as 'left' or 'backspace' instead of a\n character sequence\n\n-K or --key-names\n Display a list of available key names. Specifying -a or --all\n includes keys that don't have a known mapping\n\n-f or --function-names\n Display a list of available input functions\n\n-L or --list-modes\n Display a list of defined bind modes\n\n-M MODE or --mode MODE\n Specify a bind mode that the bind is used in. Defaults to\n \"default\"\n\n-m NEW_MODE or --sets-mode NEW_MODE\n Change the current mode to NEW_MODE after this binding is\n executed\n\n-e or --erase\n Erase the binding with the given sequence and mode instead of\n defining a new one. Multiple sequences can be specified with\n this flag. Specifying -a or --all with -M or --mode erases\n all binds in the given mode regardless of sequence.\n Specifying -a or --all without -M or --mode erases all binds\n in all modes regardless of sequence.\n\n-a or --all\n See --erase and --key-names\n\n--preset and --user\n Specify if bind should operate on user or preset bindings.\n User bindings take precedence over preset bindings when fish\n looks up mappings. By default, all bind invocations work on\n the \"user\" level except for listing, which will show both\n levels. All invocations except for inserting new bindings can\n operate on both levels at the same time (if both --preset and\n --user are given). --preset should only be used in full\n binding sets (like when working on fish_vi_key_bindings).\n\n-s or --silent\n Silences some of the error messages, including for unknown key\n names and unbound sequences.\n\n-h or --help\n Displays help about using this command.\n\nSPECIAL INPUT FUNCTIONS\nThe following special input functions are available:\n\nand only execute the next function if the previous succeeded\n (note: only some functions report success)\n\naccept-autosuggestion\n accept the current autosuggestion\n\nbackward-char\n move one character to the left. If the completion pager is\n active, select the previous completion instead.\n\nbackward-bigword\n move one whitespace-delimited word to the left\n\nbackward-delete-char\n deletes one character of input to the left of the cursor\n\nbackward-kill-bigword\n move the whitespace-delimited word to the left of the cursor\n to the killring\n\nbackward-kill-line\n move everything from the beginning of the line to the cursor\n to the killring\n\nbackward-kill-path-component\n move one path component to the left of the cursor to the\n killring. A path component is everything likely to belong to a\n path component, i.e. not any of the following: /={,}'\":@\n |;<>&, plus newlines and tabs.\n\nbackward-kill-word\n move the word to the left of the cursor to the killring. The\n \"word\" here is everything up to punctuation or whitespace.\n\nbackward-word\n move one word to the left\n\nbeginning-of-buffer\n moves to the beginning of the buffer, i.e. the start of the\n first line\n\nbeginning-of-history\n move to the beginning of the history\n\nbeginning-of-line\n move to the beginning of the line\n\nbegin-selection\n start selecting text\n\ncancel cancel the current commandline and replace it with a new empty\n one\n\ncancel-commandline\n cancel the current commandline and replace it with a new empty\n one, leaving the old one in place with a marker to show that\n it was cancelled\n\ncapitalize-word\n make the current word begin with a capital letter\n\nclear-screen\n clears the screen and redraws the prompt. if the terminal\n doesn't support clearing the screen it is the same as repaint.\n\ncomplete\n guess the remainder of the current token\n\ncomplete-and-search\n invoke the searchable pager on completion options (for\n convenience, this also moves backwards in the completion\n pager)\n\ndelete-char\n delete one character to the right of the cursor\n\ndelete-or-exit\n delete one character to the right of the cursor, or exit the\n shell if the commandline is empty\n\ndown-line\n move down one line\n\ndowncase-word\n make the current word lowercase\n\nend-of-buffer\n moves to the end of the buffer, i.e. the end of the first line\n\nend-of-history\n move to the end of the history\n\nend-of-line\n move to the end of the line\n\nend-selection\n end selecting text\n\nexpand-abbr\n expands any abbreviation currently under the cursor\n\nexecute\n run the current commandline\n\nexit exit the shell\n\nforward-bigword\n move one whitespace-delimited word to the right\n\nforward-char\n move one character to the right; or if at the end of the\n commandline, accept the current autosuggestion. If the\n completion pager is active, select the next completion\n instead.\n\nforward-single-char\n move one character to the right; or if at the end of the\n commandline, accept a single char from the current\n autosuggestion.\n\nforward-word\n move one word to the right; or if at the end of the\n commandline, accept one word from the current autosuggestion.\n\nhistory-pager\n invoke the searchable pager on history (incremental search);\n or if the history pager is already active, search further\n backwards in time.\n\nhistory-pager-delete\n permanently delete the history item selected in the history\n pager\n\nhistory-search-backward\n search the history for the previous match\n\nhistory-search-forward\n search the history for the next match\n\nhistory-prefix-search-backward\n search the history for the previous prefix match\n\nhistory-prefix-search-forward\n search the history for the next prefix match\n\nhistory-token-search-backward\n search the history for the previous matching argument\n\nhistory-token-search-forward\n search the history for the next matching argument\n\nforward-jump and backward-jump\n read another character and jump to its next occurence\n after/before the cursor\n\nforward-jump-till and backward-jump-till\n jump to right before the next occurence\n\nrepeat-jump and repeat-jump-reverse\n redo the last jump in the same/opposite direction\n\nkill-bigword\n move the next whitespace-delimited word to the killring\n\nkill-line\n move everything from the cursor to the end of the line to the\n killring\n\nkill-selection\n move the selected text to the killring\n\nkill-whole-line\n move the line (including the following newline) to the\n killring. If the line is the last line, its preceeding newline\n is also removed\n\nkill-inner-line\n move the line (without the following newline) to the killring\n\nkill-word\n move the next word to the killring\n\nnextd-or-forward-word\n if the commandline is empty, then move forward in the\n directory history, otherwise move one word to the right; or if\n at the end of the commandline, accept one word from the\n current autosuggestion.\n\nor only execute the next function if the previous did not succeed\n (note: only some functions report failure)\n\npager-toggle-search\n toggles the search field if the completions pager is visible;\n or if used after history-pager, search forwards in time.\n\nprevd-or-backward-word\n if the commandline is empty, then move backward in the\n directory history, otherwise move one word to the left\n\nrepaint\n reexecutes the prompt functions and redraws the prompt (also\n force-repaint for backwards-compatibility)\n\nrepaint-mode\n reexecutes the fish_mode_prompt and redraws the prompt. This\n is useful for vi-mode. If no fish_mode_prompt exists or it\n prints nothing, it acts like a normal repaint.\n\nself-insert\n inserts the matching sequence into the command line\n\nself-insert-notfirst\n inserts the matching sequence into the command line, unless\n the cursor is at the beginning\n\nsuppress-autosuggestion\n remove the current autosuggestion. Returns true if there was a\n suggestion to remove.\n\nswap-selection-start-stop\n go to the other end of the highlighted text without changing\n the selection\n\ntranspose-chars\n transpose two characters to the left of the cursor\n\ntranspose-words\n transpose two words to the left of the cursor\n\ntogglecase-char\n toggle the capitalisation (case) of the character under the\n cursor\n\ntogglecase-selection\n toggle the capitalisation (case) of the selection\n\ninsert-line-under\n add a new line under the current line\n\ninsert-line-over\n add a new line over the current line\n\nup-line\n move up one line\n\nundo and redo\n revert or redo the most recent edits on the command line\n\nupcase-word\n make the current word uppercase\n\nyank insert the latest entry of the killring into the buffer\n\nyank-pop\n rotate to the previous entry of the killring\n\nADDITIONAL FUNCTIONS\nThe following functions are included as normal functions, but are\nparticularly useful for input editing:\n\nup-or-search and down-or-search\n move the cursor or search the history depending on the cursor\n position and current mode\n\nedit_command_buffer\n open the visual editor (controlled by the VISUAL or EDITOR\n environment variables) with the current command-line contents\n\nfish_clipboard_copy\n copy the current selection to the system clipboard\n\nfish_clipboard_paste\n paste the current selection from the system clipboard before\n the cursor\n\nfish_commandline_append\n append the argument to the command-line. If the command-line\n already ends with the argument, this removes the suffix\n instead. Starts with the last command from history if the\n command-line is empty.\n\nfish_commandline_prepend\n prepend the argument to the command-line. If the command-line\n already starts with the argument, this removes the prefix\n instead. Starts with the last command from history if the\n command-line is empty.\n\nEXAMPLES\nExit the shell when Control+D is pressed:\n\n bind \\cd 'exit'\n\nPerform a history search when Page Up is pressed:\n\n bind -k ppage history-search-backward\n\nTurn on Vi key bindings and rebind Control+C to clear the input line:\n\n set -g fish_key_bindings fish_vi_key_bindings\n bind -M insert \\cc kill-whole-line repaint\n\nLaunch git diff and repaint the commandline afterwards when Control+G\nis pressed:\n\n bind \\cg 'git diff; commandline -f repaint'\n\nTERMINAL LIMITATIONS\nUnix terminals, like the ones fish operates in, are at heart 70s\ntechnology. They have some limitations that applications running\ninside them can't workaround.\n\nFor instance, the control key modifies a character by setting the top\nthree bits to 0. This means:\n\n• Many characters + control are indistinguishable from other keys.\n Control+I is tab, Control+J is newline (\\n).\n\n• Control and shift don't work simultaneously\n\nOther keys don't have a direct encoding, and are sent as escape\nsequences. For example → (Right) often sends \\e\\[C. These can differ\nfrom terminal to terminal, and the mapping is typically available in\nterminfo(5). Sometimes however a terminal identifies as e.g.\nxterm-256color for compatibility, but then implements xterm's\nsequences incorrectly.\n\nSPECIAL CASE: THE ESCAPE CHARACTER\nThe escape key can be used standalone, for example, to switch from\ninsertion mode to normal mode when using Vi keybindings. Escape can\nalso be used as a \"meta\" key, to indicate the start of an escape\nsequence, like for function or arrow keys. Custom bindings can also\nbe defined that begin with an escape character.\n\nHolding alt and something else also typically sends escape, for\nexample holding alt+a will send an escape character and then an \"a\".\n\nfish waits for a period after receiving the escape character, to\ndetermine whether it is standalone or part of an escape sequence.\nWhile waiting, additional key presses make the escape key behave as a\nmeta key. If no other key presses come in, it is handled as a\nstandalone escape. The waiting period is set to 30 milliseconds (0.03\nseconds). It can be configured by setting the fish_escape_delay_ms\nvariable to a value between 10 and 5000 ms. This can be a universal\nvariable that you set once from an interactive session. So the\nescape character has its own timeout configured with\nfish_escape_delay_ms.\n\nSee also Key sequences.", + "args": "bind [(-M | --mode) MODE] [(-m | --sets-mode) NEW_MODE] [--preset | --user] [-s | --silent] [-k | --key] SEQUENCE COMMAND ...\nbind [(-M | --mode) MODE] [-k | --key] [--preset] [--user] SEQUENCE\nbind (-K | --key-names) [-a | --all] [--preset] [--user]\nbind (-f | --function-names)\nbind (-L | --list-modes)\nbind (-e | --erase) [(-M | --mode) MODE] [--preset] [--user] [-a | --all] | [-k | --key] SEQUENCE ..." + }, + "block": { + "shortDescription": "temporarily block delivery of events", + "description": "block prevents events triggered by fish or the emit command from\nbeing delivered and acted upon while the block is in place.\n\nIn functions, block can be useful while performing work that should\nnot be interrupted by the shell.\n\nThe block can be removed. Any events which triggered while the block\nwas in place will then be delivered.\n\nEvent blocks should not be confused with code blocks, which are\ncreated with begin, if, while or for\n\nWithout options, the block command acts with function scope.\n\nThe following options are available:\n\n-l or --local\n Release the block automatically at the end of the current\n innermost code block scope.\n\n-g or --global\n Never automatically release the lock.\n\n-e or --erase\n Release global block.\n\n-h or --help\n Displays help about using this command.\n\nEXAMPLE\n\n # Create a function that listens for events\n function --on-event foo foo; echo 'foo fired'; end\n\n # Block the delivery of events\n block -g\n\n emit foo\n # No output will be produced\n\n block -e\n # 'foo fired' will now be printed\n\nNOTES\nEvents are only received from the current fish process as there is no\nway to send events from one fish process to another (yet).", + "args": "block [(--local | --global)]\nblock --erase" + }, + "break": { + "shortDescription": "Exit the current loop", + "description": "Terminate the execution of the nearest enclosing `while` or `for` loop and proceed with the next command after the loop." + }, + "breakpoint": { + "shortDescription": "Launch debug mode", + "description": "Pause execution and launch an interactive debug prompt. This is useful for inspecting the state of a script at a specific point." + }, + "builtin": { + "shortDescription": "run a builtin command", + "description": "builtin forces the shell to use a builtin command named BUILTIN,\nrather than a function or external program.\n\nThe following options are available:\n\n-n or --names\n Lists the names of all defined builtins.\n\n-q or --query BUILTIN\n Tests if any of the specified builtins exist. If any exist, it\n returns 0, 1 otherwise.\n\n-h or --help\n Displays help about using this command.\n\nEXAMPLE\n\n builtin jobs\n # executes the jobs builtin, even if a function named jobs exists", + "args": "builtin [OPTIONS] BUILTINNAME\nbuiltin --query BUILTINNAME ...\nbuiltin --names" + }, + "case": { + "shortDescription": "Match a value against patterns", + "description": "Within a `switch` block, the `case` command specifies patterns to match against the given value, executing the associated block if a match is found.", + "args": "PATTERN..." + }, + "cd": { + "shortDescription": "change directory", + "description": "cd changes the current working directory.\n\nIf DIRECTORY is given, it will become the new directory. If no\nparameter is given, the HOME environment variable will be used.\n\nIf DIRECTORY is a relative path, all the paths in the CDPATH will be\ntried as prefixes for it, in addition to PWD. It is recommended to\nkeep . as the first element of CDPATH, or PWD will be tried last.\n\nFish will also try to change directory if given a command that looks\nlike a directory (starting with ., / or ~, or ending with /), without\nexplicitly requiring cd.\n\nFish also ships a wrapper function around the builtin cd that\nunderstands cd - as changing to the previous directory. See also\nprevd. This wrapper function maintains a history of the 25 most\nrecently visited directories in the $dirprev and $dirnext global\nvariables. If you make those universal variables your cd history is\nshared among all fish instances.\n\nAs a special case, cd . is equivalent to cd $PWD, which is useful in\ncases where a mountpoint has been recycled or a directory has been\nremoved and recreated.\n\nThe --help or -h option displays help about using this command, and\ndoes not change the directory.\n\nEXAMPLES\n\n cd\n # changes the working directory to your home directory.\n\n cd /usr/src/fish-shell\n # changes the working directory to /usr/src/fish-shell\n\nSEE ALSO\nNavigate directories using the directory history or the directory\nstack", + "args": "cd [DIRECTORY]" + }, + "command": { + "shortDescription": "run a program", + "description": "command forces the shell to execute the program COMMANDNAME and\nignore any functions or builtins with the same name.\n\nThe following options are available:\n\n-a or --all\n Prints all COMMAND found in PATH, in the order found.\n\n-q or --query\n Silence output and print nothing, setting only exit status.\n Implies --search. For compatibility, this is also --quiet\n (deprecated).\n\n-v (or -s or --search)\n Prints the external command that would be executed, or prints\n nothing if no file with the specified name could be found in\n PATH.\n\n-h or --help\n Displays help about using this command.\n\nWith the -v option, command treats every argument as a separate\ncommand to look up and sets the exit status to 0 if any of the\nspecified commands were found, or 127 if no commands could be found.\n--quiet used with -v prevents commands being printed, like type -q.\n\nEXAMPLES\ncommand ls executes the ls program, even if an ls function also exists.\ncommand -s ls prints the path to the ls program.\ncommand -q git; and command git log runs git log only if git exists.", + "args": "command [OPTIONS] [COMMANDNAME [ARG ...]]" + }, + "commandline": { + "shortDescription": "set or get the current command line buffer", + "description": "commandline can be used to set or get the current contents of the\ncommand line buffer.\n\nWith no parameters, commandline returns the current value of the\ncommand line.\n\nWith CMD specified, the command line buffer is erased and replaced\nwith the contents of CMD.\n\nThe following options are available:\n\n-C or --cursor\n Set or get the current cursor position, not the contents of\n the buffer. If no argument is given, the current cursor\n position is printed, otherwise the argument is interpreted as\n the new cursor position. If one of the options -j, -p or -t\n is given, the position is relative to the respective substring\n instead of the entire command line buffer.\n\n-B or --selection-start\n Get current position of the selection start in the buffer.\n\n-E or --selection-end\n Get current position of the selection end in the buffer.\n\n-f or --function\n Causes any additional arguments to be interpreted as input\n functions, and puts them into the queue, so that they will be\n read before any additional actual key presses are. This\n option cannot be combined with any other option. See bind for\n a list of input functions.\n\n-h or --help\n Displays help about using this command.\n\nThe following options change the way commandline updates the command\nline buffer:\n\n-a or --append\n Do not remove the current commandline, append the specified\n string at the end of it.\n\n-i or --insert\n Do not remove the current commandline, insert the specified\n string at the current cursor position\n\n-r or --replace\n Remove the current commandline and replace it with the\n specified string (default)\n\nThe following options change what part of the commandline is printed\nor updated:\n\n-b or --current-buffer\n Select the entire commandline, not including any displayed\n autosuggestion (default).\n\n-j or --current-job\n Select the current job - a job here is one pipeline. Stops at\n logical operators or terminators (;, &, and newlines).\n\n-p or --current-process\n Select the current process - a process here is one command.\n Stops at logical operators, terminators, and pipes.\n\n-s or --current-selection\n Selects the current selection\n\n-t or --current-token\n Selects the current token\n\nThe following options change the way commandline prints the current\ncommandline buffer:\n\n-c or --cut-at-cursor\n Only print selection up until the current cursor position. If\n combined with --tokenize, this will print up until the last\n completed token - excluding the token the cursor is in. This\n is typically what you would want for instance in completions.\n To get both, use both commandline --cut-at-cursor --tokenize;\n commandline --cut-at-cursor --current-token, or commandline\n -co; commandline -ct for short.\n\n-o or --tokenize\n Tokenize the selection and print one string-type token per\n line.\n\nIf commandline is called during a call to complete a given string\nusing complete -C STRING, commandline will consider the specified\nstring to be the current contents of the command line.\n\nThe following options output metadata about the commandline state:\n\n-L or --line\n Print the line that the cursor is on, with the topmost line\n starting at 1.\n\n-S or --search-mode\n Evaluates to true if the commandline is performing a history\n search.\n\n-P or --paging-mode\n Evaluates to true if the commandline is showing pager\n contents, such as tab completions.\n\n--paging-full-mode\n Evaluates to true if the commandline is showing pager\n contents, such as tab completions and all lines are shown (no\n \" more rows\" message).\n\n--is-valid\n Returns true when the commandline is syntactically valid and\n complete. If it is, it would be executed when the execute\n bind function is called. If the commandline is incomplete,\n return 2, if erroneus, return 1.\n\nEXAMPLE\ncommandline -j $history[3] replaces the job under the cursor with the\nthird item from the command line history.\n\nIf the commandline contains\n\n > echo $flounder >&2 | less; and echo $catfish\n\n(with the cursor on the \"o\" of \"flounder\")\n\nThe echo $flounder >& is the first process, less the second and and\necho $catfish the third.\n\necho $flounder >&2 | less is the first job, and echo $catfish the\nsecond.\n\n$flounder is the current token.\n\nThe most common use for something like completions is\n\n set -l tokens (commandline -opc)\n\nwhich gives the current process (what is being completed), tokenized\ninto separate entries, up to but excluding the currently being\ncompleted token\n\nIf you are then also interested in the in-progress token, add\n\n:: set -l current (commandline -ct)\n\nNote that this makes it easy to render fish's infix matching moot -\nif possible it's best if the completions just print all possibilities\nand leave the matching to the current token up to fish's logic.\n\nMore examples:\n\n > commandline -t\n $flounder\n > commandline -ct\n $fl\n > commandline -b # or just commandline\n echo $flounder >&2 | less; and echo $catfish\n > commandline -p\n echo $flounder >&2\n > commandline -j\n echo $flounder >&2 | less", + "args": "commandline [OPTIONS] [CMD]" + }, + "complete": { + "shortDescription": "edit command-specific tab-completions", + "description": "complete defines, removes or lists completions for a command.\n\nFor an introduction to writing your own completions, see Writing your\nown completions in the fish manual.\n\nThe following options are available:\n\n-c or --command COMMAND\n Specifies that COMMAND is the name of the command. If there is\n no -c or -p, one non-option argument will be used as the\n command.\n\n-p or --path COMMAND\n Specifies that COMMAND is the absolute path of the command\n (optionally containing wildcards).\n\n-e or --erase\n Deletes the specified completion.\n\n-s or --short-option SHORT_OPTION\n Adds a short option to the completions list.\n\n-l or --long-option LONG_OPTION\n Adds a GNU-style long option to the completions list.\n\n-o or --old-option OPTION\n Adds an old-style short or long option (see below for\n details).\n\n-a or --arguments ARGUMENTS\n Adds the specified option arguments to the completions list.\n\n-k or --keep-order\n Keeps the order of ARGUMENTS instead of sorting\n alphabetically. Multiple complete calls with -k result in\n arguments of the later ones displayed first.\n\n-f or --no-files\n This completion may not be followed by a filename.\n\n-F or --force-files\n This completion may be followed by a filename, even if another\n applicable complete specified --no-files.\n\n-r or --require-parameter\n This completion must have an option argument, i.e. may not be\n followed by another option.\n\n-x or --exclusive\n Short for -r and -f.\n\n-w or --wraps WRAPPED_COMMAND\n Causes the specified command to inherit completions from\n WRAPPED_COMMAND (see below for details).\n\n-n or --condition CONDITION\n This completion should only be used if the CONDITION (a shell\n command) returns 0. This makes it possible to specify\n completions that should only be used in some cases. If\n multiple conditions are specified, fish will try them in the\n order they are specified until one fails or all succeeded.\n\n-C or --do-complete STRING\n Makes complete try to find all possible completions for the\n specified string. If there is no STRING, the current\n commandline is used instead.\n\n--escape\n When used with -C, escape special characters in completions.\n\n-h or --help\n Displays help about using this command.\n\nCommand-specific tab-completions in fish are based on the notion of\noptions and arguments. An option is a parameter which begins with a\nhyphen, such as -h, -help or --help. Arguments are parameters that do\nnot begin with a hyphen. Fish recognizes three styles of options, the\nsame styles as the GNU getopt library. These styles are:\n\n• Short options, like -a. Short options are a single character long,\n are preceded by a single hyphen and can be grouped together (like\n -la, which is equivalent to -l -a). Option arguments may be\n specified by appending the option with the value (-w32), or, if\n --require-parameter is given, in the following parameter (-w 32).\n\n• Old-style options, long like -Wall or -name or even short like -a.\n Old-style options can be more than one character long, are preceded\n by a single hyphen and may not be grouped together. Option\n arguments are specified by default following a space (-foo null) or\n after = (-foo=null).\n\n• GNU-style long options, like --colors. GNU-style long options can\n be more than one character long, are preceded by two hyphens, and\n can't be grouped together. Option arguments may be specified after\n a = (--quoting-style=shell), or, if --require-parameter is given,\n in the following parameter (--quoting-style shell).\n\nMultiple commands and paths can be given in one call to define the\nsame completions for multiple commands.\n\nMultiple command switches and wrapped commands can also be given to\ndefine multiple completions in one call.\n\nInvoking complete multiple times for the same command adds the new\ndefinitions on top of any existing completions defined for the\ncommand.\n\nWhen -a or --arguments is specified in conjunction with long, short,\nor old-style options, the specified arguments are only completed as\narguments for any of the specified options. If -a or --arguments is\nspecified without any long, short, or old-style options, the\nspecified arguments are used when completing non-option arguments to\nthe command (except when completing an option argument that was\nspecified with -r or --require-parameter).\n\nCommand substitutions found in ARGUMENTS should return a\nnewline-separated list of arguments, and each argument may optionally\nhave a tab character followed by the argument description.\nDescription given this way override a description given with -d or\n--description.\n\nDescriptions given with --description are also used to group options\ngiven with -s, -o or -l. Options with the same (non-empty)\ndescription will be listed as one candidate, and one of them will be\npicked. If the description is empty or no description was given this\nis skipped.\n\nThe -w or --wraps options causes the specified command to inherit\ncompletions from another command, \"wrapping\" the other command. The\nwrapping command can also have additional completions. A command can\nwrap multiple commands, and wrapping is transitive: if A wraps B, and\nB wraps C, then A automatically inherits all of C's completions.\nWrapping can be removed using the -e or --erase options. Wrapping\nonly works for completions specified with -c or --command and are\nignored when specifying completions with -p or --path.\n\nWhen erasing completions, it is possible to either erase all\ncompletions for a specific command by specifying complete -c COMMAND\n-e, or by specifying a specific completion option to delete.\n\nWhen complete is called without anything that would define or erase\ncompletions (options, arguments, wrapping, ...), it shows matching\ncompletions instead. So complete without any arguments shows all\nloaded completions, complete -c foo shows all loaded completions for\nfoo. Since completions are autoloaded, you will have to trigger them\nfirst.\n\nEXAMPLES\nThe short-style option -o for the gcc command needs a file argument:\n\n complete -c gcc -s o -r\n\nThe short-style option -d for the grep command requires one of read,\nskip or recurse:\n\n complete -c grep -s d -x -a \"read skip recurse\"\n\nThe su command takes any username as an argument. Usernames are given\nas the first colon-separated field in the file /etc/passwd. This can\nbe specified as:\n\n complete -x -c su -d \"Username\" -a \"(cat /etc/passwd | cut -d : -f 1)\"\n\nThe rpm command has several different modes. If the -e or --erase\nflag has been specified, rpm should delete one or more packages, in\nwhich case several switches related to deleting packages are valid,\nlike the nodeps switch.\n\nThis can be written as:\n\n complete -c rpm -n \"__fish_contains_opt -s e erase\" -l nodeps -d \"Don't check dependencies\"\n\nwhere __fish_contains_opt is a function that checks the command line\nbuffer for the presence of a specified set of options.\n\nTo implement an alias, use the -w or --wraps option:\n\n complete -c hub -w git\n\nNow hub inherits all of the completions from git. Note this can also\nbe specified in a function declaration (function thing -w\notherthing).\n\n complete -c git\n\nShows all completions for git.\n\nAny command foo that doesn't support grouping multiple short options\nin one string (not supporting -xf as short for -x -f) or a short\noption and its value in one string (not supporting -d9 instead of -d\n9) should be specified as a single-character old-style option instead\nof as a short-style option; for example, complete -c foo -o s;\ncomplete -c foo -o v would never suggest foo -ov but rather foo -o\n-v.", + "args": "complete ((-c | --command) | (-p | --path)) COMMAND [OPTIONS]\ncomplete (-C | --do-complete) [--escape] STRING" + }, + "contains": { + "shortDescription": "test if a word is present in a list", + "description": "contains tests whether the set VALUES contains the string KEY. If\nso, contains exits with code 0; if not, it exits with code 1.\n\nThe following options are available:\n\n-i or --index\n Print the index (number of the element in the set) of the\n first matching element.\n\n-h or --help\n Displays help about using this command.\n\nNote that contains interprets all arguments starting with a - as an\noption to contains, until an -- argument is reached.\n\nSee the examples below.\n\nEXAMPLE\nIf animals is a list of animals, the following will test if animals\ncontains \"cat\":\n\n if contains cat $animals\n echo Your animal list is evil!\n end\n\nThis code will add some directories to PATH if they aren't yet\nincluded:\n\n for i in ~/bin /usr/local/bin\n if not contains $i $PATH\n set PATH $PATH $i\n end\n end\n\nWhile this will check if function hasargs is being ran with the -q\noption:\n\n function hasargs\n if contains -- -q $argv\n echo '$argv contains a -q option'\n end\n end\n\nThe -- here stops contains from treating -q to an option to itself.\nInstead it treats it as a normal string to check.", + "args": "contains [OPTIONS] KEY [VALUES ...]" + }, + "continue": { + "shortDescription": "Skip to the next iteration of a loop", + "description": "Within a `while` or `for` loop, `continue` skips the remaining commands in the current iteration and proceeds to the next iteration of the loop." + }, + "count": { + "shortDescription": "", + "description": "" + }, + "disown": { + "shortDescription": "remove a process from the list of jobs", + "description": "disown removes the specified job from the list of jobs. The job\nitself continues to exist, but fish does not keep track of it any\nlonger.\n\nJobs in the list of jobs are sent a hang-up signal when fish\nterminates, which usually causes the job to terminate; disown allows\nthese processes to continue regardless.\n\nIf no process is specified, the most recently-used job is removed\n(like bg and fg). If one or more PIDs are specified, jobs with the\nspecified process IDs are removed from the job list. Invalid jobs are\nignored and a warning is printed.\n\nIf a job is stopped, it is sent a signal to continue running, and a\nwarning is printed. It is not possible to use the bg builtin to\ncontinue a job once it has been disowned.\n\ndisown returns 0 if all specified jobs were disowned successfully,\nand 1 if any problems were encountered.\n\nThe --help or -h option displays help about using this command.\n\nEXAMPLE\nfirefox &; disown will start the Firefox web browser in the\nbackground and remove it from the job list, meaning it will not be\nclosed when the fish process is closed.\n\ndisown (jobs -p) removes all jobs from the job list without\nterminating them.", + "args": "disown [PID ...]" + }, + "echo": { + "shortDescription": "", + "description": "" + }, + "else": { + "shortDescription": "Execute commands if the previous condition was false", + "description": "In an `if` block, the `else` section contains commands that execute if none of the preceding `if` or `else if` conditions were true." + }, + "emit": { + "shortDescription": "emit a generic event", + "description": "emit emits, or fires, an event. Events are delivered to, or caught\nby, special functions called event handlers. The arguments are passed\nto the event handlers as function arguments.\n\nThe --help or -h option displays help about using this command.\n\nEXAMPLE\nThe following code first defines an event handler for the generic\nevent named 'test_event', and then emits an event of that type.\n\n function event_test --on-event test_event\n echo event test: $argv\n end\n\n emit test_event something\n\nNOTES\nNote that events are only sent to the current fish process as there\nis no way to send events from one fish process to another.", + "args": "emit EVENT_NAME [ARGUMENTS ...]" + }, + "end": { + "shortDescription": "Terminate a block of code", + "description": "Conclude a block of code initiated by constructs like `if`, `switch`, `while`, `for`, or `function`." + }, + "eval": { + "shortDescription": "Execute arguments as a command", + "description": "Concatenate all arguments into a single command and execute it. This allows for dynamic construction and execution of commands.", + "args": "COMMAND..." + }, + "exec": { + "shortDescription": "execute command in current process", + "description": "exec replaces the currently running shell with a new command. On\nsuccessful completion, exec never returns. exec cannot be used inside\na pipeline.\n\nThe --help or -h option displays help about using this command.\n\nEXAMPLE\nexec emacs starts up the emacs text editor, and exits fish. When\nemacs exits, the session will terminate.", + "args": "exec COMMAND" + }, + "exit": { + "shortDescription": "exit the shell", + "description": "exit is a special builtin that causes the shell to exit. Either 255\nor the CODE supplied is used, whichever is lesser. Otherwise, the\nexit status will be that of the last command executed.\n\nIf exit is called while sourcing a file (using the source builtin)\nthe rest of the file will be skipped, but the shell itself will not\nexit.\n\nThe --help or -h option displays help about using this command.", + "args": "exit [CODE]" + }, + "false": { + "shortDescription": "Return an unsuccessful result", + "description": "A command that returns a non-zero exit status, indicating failure. It is often used in scripts to represent a false condition." + }, + "fg": { + "shortDescription": "bring job to foreground", + "description": "The fg builtin brings the specified job to the foreground, resuming\nit if it is stopped. While a foreground job is executed, fish is\nsuspended. If no job is specified, the last job to be used is put in\nthe foreground. If PID is specified, the job containing a process\nwith the specified process ID is put in the foreground.\n\nFor compatibility with other shells, job expansion syntax is\nsupported for fg. A PID of the format %1 will foreground job 1. Job\nnumbers can be seen in the output of jobs.\n\nThe --help or -h option displays help about using this command.\n\nEXAMPLE\nfg will put the last job in the foreground.\n\nfg %3 will put job 3 into the foreground.", + "args": "fg [PID]" + }, + "for": { + "shortDescription": "perform a set of commands multiple times", + "description": "for is a loop construct. It will perform the commands specified by\nCOMMANDS multiple times. On each iteration, the local variable\nspecified by VARNAME is assigned a new value from VALUES. If VALUES\nis empty, COMMANDS will not be executed at all. The VARNAME is\nvisible when the loop terminates and will contain the last value\nassigned to it. If VARNAME does not already exist it will be set in\nthe local scope. For our purposes if the for block is inside a\nfunction there must be a local variable with the same name. If the\nfor block is not nested inside a function then global and universal\nvariables of the same name will be used if they exist.\n\nMuch like set, for does not modify $status, but the evaluation of its\nsubordinate commands can.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\n\n for i in foo bar baz; echo $i; end\n\n # would output:\n foo\n bar\n baz\n\nNOTES\nThe VARNAME was local to the for block in releases prior to 3.0.0.\nThis means that if you did something like this:\n\n for var in a b c\n if break_from_loop\n break\n end\n end\n echo $var\n\nThe last value assigned to var when the loop terminated would not be\navailable outside the loop. What echo $var would write depended on\nwhat it was set to before the loop was run. Likely nothing.", + "args": "for VARNAME in [VALUES ...]; COMMANDS ...; end" + }, + "function": { + "shortDescription": "create a function", + "description": "function creates a new function NAME with the body BODY.\n\nA function is a list of commands that will be executed when the name\nof the function is given as a command.\n\nThe following options are available:\n\n-a NAMES or --argument-names NAMES\n Assigns the value of successive command-line arguments to the\n names given in NAMES. These are the same arguments given in\n argv, and are still available there. See also Argument\n Handling.\n\n-d DESCRIPTION or --description DESCRIPTION\n A description of what the function does, suitable as a\n completion description.\n\n-w WRAPPED_COMMAND or --wraps WRAPPED_COMMAND\n Inherit completions from the given WRAPPED_COMMAND. See the\n documentation for complete for more information.\n\n-e EVENT_NAME or --on-event EVENT_NAME\n Run this function when the specified named event is emitted.\n Fish internally generates named events, for example, when\n showing the prompt. Custom events can be emitted using the\n emit command.\n\n-v VARIABLE_NAME or --on-variable VARIABLE_NAME\n Run this function when the variable VARIABLE_NAME changes\n value. Note that fish makes no guarantees on any particular\n timing or even that the function will be run for every single\n set. Rather it will be run when the variable has been set at\n least once, possibly skipping some values or being run when\n the variable has been set to the same value (except for\n universal variables set in other shells - only changes in the\n value will be picked up for those).\n\n-j PID or --on-job-exit PID\n Run this function when the job containing a child process with\n the given process identifier PID exits. Instead of a PID, the\n string 'caller' can be specified. This is only allowed when in\n a command substitution, and will result in the handler being\n triggered by the exit of the job which created this command\n substitution.\n\n-p PID or --on-process-exit PID\n Run this function when the fish child process with process ID\n PID exits. Instead of a PID, for backward compatibility,\n \"%self\" can be specified as an alias for $fish_pid, and the\n function will be run when the current fish instance exits.\n\n-s SIGSPEC or --on-signal SIGSPEC\n Run this function when the signal SIGSPEC is delivered.\n SIGSPEC can be a signal number, or the signal name, such as\n SIGHUP (or just HUP). Note that the signal must have been\n delivered to fish; for example, Ctrl-C sends SIGINT to the\n foreground process group, which will not be fish if you are\n running another command at the time. Observing a signal will\n prevent fish from exiting in response to that signal.\n\n-S or --no-scope-shadowing\n Allows the function to access the variables of calling\n functions. Normally, any variables inside the function that\n have the same name as variables from the calling function are\n \"shadowed\", and their contents are independent of the calling\n function.\n\n It's important to note that this does not capture referenced\n variables or the scope at the time of function declaration! At\n this time, fish does not have any concept of closures, and\n variable lifetimes are never extended. In other words, by\n using --no-scope-shadowing the scope of the function each time\n it is run is shared with the scope it was called from rather\n than the scope it was defined in.\n\n-V or --inherit-variable NAME\n Snapshots the value of the variable NAME and defines a local\n variable with that same name and value when the function is\n defined. This is similar to a closure in other languages like\n Python but a bit different. Note the word \"snapshot\" in the\n first sentence. If you change the value of the variable after\n defining the function, even if you do so in the same scope\n (typically another function) the new value will not be used by\n the function you just created using this option. See the\n function notify example below for how this might be used.\n\nThe event handler switches (on-event, on-variable, on-job-exit,\non-process-exit and on-signal) cause a function to run automatically\nat specific events. New named events for --on-event can be fired\nusing the emit builtin. Fish already generates a few events, see\nEvent handlers for more.\n\nFunctions may not be named the same as a reserved keyword. These are\nelements of fish syntax or builtin commands which are essential for\nthe operations of the shell. Current reserved words are [, , and,\nargparse, begin, break, builtin, case, command, continue, else, end,\neval, exec, for, function, if, not, or, read, return, set, status,\nstring, switch, test, time, and while.\n\nEXAMPLE\n\n function ll\n ls -l $argv\n end\n\nwill run the ls command, using the -l option, while passing on any\nadditional files and switches to ls.\n\n function mkdir -d \"Create a directory and set CWD\"\n command mkdir $argv\n if test $status = 0\n switch $argv[(count $argv)]\n case '-*'\n\n case '*'\n cd $argv[(count $argv)]\n return\n end\n end\n end\n\nThis will run the mkdir command, and if it is successful, change the\ncurrent working directory to the one just created.\n\n function notify\n set -l job (jobs -l -g)\n or begin; echo \"There are no jobs\" >&2; return 1; end\n\n function _notify_job$job --on-job-exit $job --inherit-variable job\n echo -n \\a # beep\n functions -e _notify_job$job\n end\n end\n\nThis will beep when the most recent job completes.\n\nNOTES\nEvents are only received from the current fish process as there is no\nway to send events from one fish process to another.\n\nSEE MORE\nFor more explanation of how functions fit into fish, see Functions.", + "args": "function NAME [OPTIONS]; BODY; end" + }, + "functions": { + "shortDescription": "print or erase functions", + "description": "functions prints or erases functions.\n\nThe following options are available:\n\n-a or --all\n Lists all functions, even those whose name starts with an\n underscore.\n\n-c or --copy OLDNAME NEWNAME\n Creates a new function named NEWNAME, using the definition of\n the OLDNAME function.\n\n-d or --description DESCRIPTION\n Changes the description of this function.\n\n-e or --erase\n Causes the specified functions to be erased. This also means\n that it is prevented from autoloading in the current session.\n Use funcsave to remove the saved copy.\n\n-D or --details\n Reports the path name where the specified function is defined\n or could be autoloaded, stdin if the function was defined\n interactively or on the command line or by reading standard\n input, - if the function was created via source, and n/a if\n the function isn't available. (Functions created via alias\n will return -, because alias uses source internally.) If the\n --verbose option is also specified then five lines are\n written:\n\n • the pathname as already described,\n\n • autoloaded, not-autoloaded or n/a,\n\n • the line number within the file or zero if not applicable,\n\n • scope-shadowing if the function shadows the vars in the\n calling function (the normal case if it wasn't defined with\n --no-scope-shadowing), else no-scope-shadowing, or n/a if\n the function isn't defined,\n\n • the function description minimally escaped so it is a single\n line, or n/a if the function isn't defined or has no\n description.\n\n You should not assume that only five lines will be written\n since we may add additional information to the output in the\n future.\n\n--no-details\n Turns off function path reporting, so just the definition will\n be printed.\n\n-n or --names\n Lists the names of all defined functions.\n\n-q or --query\n Tests if the specified functions exist.\n\n-v or --verbose\n Make some output more verbose.\n\n-H or --handlers\n Show all event handlers.\n\n-t or --handlers-type TYPE\n Show all event handlers matching the given TYPE.\n\n-h or --help\n Displays help about using this command.\n\nThe default behavior of functions, when called with no arguments, is\nto print the names of all defined functions. Unless the -a option is\ngiven, no functions starting with underscores are included in the\noutput.\n\nIf any non-option parameters are given, the definition of the\nspecified functions are printed.\n\nCopying a function using -c copies only the body of the function, and\ndoes not attach any event notifications from the original function.\n\nOnly one function's description can be changed in a single invocation\nof functions -d.\n\nThe exit status of functions is the number of functions specified in\nthe argument list that do not exist, which can be used in concert\nwith the -q option.\n\nEXAMPLES\n\n functions -n\n # Displays a list of currently-defined functions\n\n functions -c foo bar\n # Copies the 'foo' function to a new function called 'bar'\n\n functions -e bar\n # Erases the function ``bar``\n\nSEE MORE\nFor more explanation of how functions fit into fish, see Functions.", + "args": "functions [-a | --all] [-n | --names]\nfunctions [-D | --details] [-v] FUNCTION\nfunctions -c OLDNAME NEWNAME\nfunctions -d DESCRIPTION FUNCTION\nfunctions [-e | -q] FUNCTION ..." + }, + "history": { + "shortDescription": "show and manipulate command history", + "description": "history is used to search, delete, and otherwise manipulate the\nhistory of interactive commands.\n\nThe following operations (sub-commands) are available:\n\nsearch Returns history items matching the search string. If no search\n string is provided it returns all history items. This is the\n default operation if no other operation is specified. You only\n have to explicitly say history search if you wish to search\n for one of the subcommands. The --contains search option will\n be used if you don't specify a different search option.\n Entries are ordered newest to oldest unless you use the\n --reverse flag. If stdout is attached to a tty the output will\n be piped through your pager by the history function. The\n history builtin simply writes the results to stdout.\n\ndelete Deletes history items. The --contains search option will be\n used if you don't specify a different search option. If you\n don't specify --exact a prompt will be displayed before any\n items are deleted asking you which entries are to be deleted.\n You can enter the word \"all\" to delete all matching entries.\n You can enter a single ID (the number in square brackets) to\n delete just that single entry. You can enter more than one ID,\n or an ID range separated by a space to delete multiple\n entries. Just press [enter] to not delete anything. Note that\n the interactive delete behavior is a feature of the history\n function. The history builtin only supports --exact\n --case-sensitive deletion.\n\nmerge Immediately incorporates history changes from other sessions.\n Ordinarily fish ignores history changes from sessions started\n after the current one. This command applies those changes\n immediately.\n\nsave Immediately writes all changes to the history file. The shell\n automatically saves the history file; this option is provided\n for internal use and should not normally need to be used by\n the user.\n\nclear Clears the history file. A prompt is displayed before the\n history is erased asking you to confirm you really want to\n clear all history unless builtin history is used.\n\nclear-session\n Clears the history file from all activity of the current\n session. Note: If history merge or builtin history merge is\n run in a session, only the history after this will be erased.\n\nThe following options are available:\n\nThese flags can appear before or immediately after one of the\nsub-commands listed above.\n\n-C or --case-sensitive\n Does a case-sensitive search. The default is case-insensitive.\n Note that prior to fish 2.4.0 the default was case-sensitive.\n\n-c or --contains\n Searches items in the history that contain the specified text\n string. This is the default for the --search flag. This is not\n currently supported by the delete subcommand.\n\n-e or --exact\n Searches or deletes items in the history that exactly match\n the specified text string. This is the default for the delete\n subcommand. Note that the match is case-insensitive by\n default. If you really want an exact match, including letter\n case, you must use the -C or --case-sensitive flag.\n\n-p or --prefix\n Searches items in the history that begin with the specified\n text string. This is not currently supported by the delete\n subcommand.\n\n-t or --show-time\n Prepends each history entry with the date and time the entry\n was recorded. By default it uses the strftime format # %c%n.\n You can specify another format; e.g., --show-time=\"%Y-%m-%d\n %H:%M:%S \" or --show-time=\"%a%I%p\". The short option, -t,\n doesn't accept a strftime format string; it only uses the\n default format. Any strftime format is allowed, including %s\n to get the raw UNIX seconds since the epoch.\n\n-z or --null\n Causes history entries written by the search operations to be\n terminated by a NUL character rather than a newline. This\n allows the output to be processed by read -z to correctly\n handle multiline history entries.\n\n-*NUMBER* -n NUMBER or --max NUMBER\n Limits the matched history items to the first NUMBER matching\n entries. This is only valid for history search.\n\n-R or --reverse\n Causes the history search results to be ordered oldest to\n newest. Which is the order used by most shells. The default is\n newest to oldest.\n\n-h or --help\n Displays help for this command.\n\nEXAMPLE\n\n history clear\n # Deletes all history items\n\n history search --contains \"foo\"\n # Outputs a list of all previous commands containing the string \"foo\".\n\n history delete --prefix \"foo\"\n # Interactively deletes commands which start with \"foo\" from the history.\n # You can select more than one entry by entering their IDs separated by a space.\n\nCUSTOMIZING THE NAME OF THE HISTORY FILE\nBy default interactive commands are logged to\n$XDG_DATA_HOME/fish/fish_history (typically\n~/.local/share/fish/fish_history).\n\nYou can set the fish_history variable to another name for the current\nshell session. The default value (when the variable is unset) is fish\nwhich corresponds to $XDG_DATA_HOME/fish/fish_history. If you set it\nto e.g. fun, the history would be written to\n$XDG_DATA_HOME/fish/fun_history. An empty string means history will\nnot be stored at all. This is similar to the private session features\nin web browsers.\n\nYou can change fish_history at any time (by using set -x fish_history\n\"session_name\") and it will take effect right away. If you set it to\n\"default\", it will use the default session name (which is \"fish\").\n\nOther shells such as bash and zsh use a variable named HISTFILE for a\nsimilar purpose. Fish uses a different name to avoid conflicts and\nsignal that the behavior is different (session name instead of a file\npath). Also, if you set the var to anything other than fish or\ndefault it will inhibit importing the bash history. That's because\nthe most common use case for this feature is to avoid leaking private\nor sensitive history when giving a presentation.\n\nNOTES\nIf you specify both --prefix and --contains the last flag seen is\nused.\n\nNote that for backwards compatibility each subcommand can also be\nspecified as a long option. For example, rather than history search\nyou can type history --search. Those long options are deprecated and\nwill be removed in a future release.", + "args": "history [search] [--show-time] [--case-sensitive]\n[--exact | --prefix | --contains] [--max N] [--null] [--reverse]\n [SEARCH_STRING ...]\nhistory delete [--case-sensitive]\n [--exact | --prefix | --contains] SEARCH_STRING ...\nhistory merge\nhistory save\nhistory clear\nhistory clear-session" + }, + "if": { + "shortDescription": "conditionally execute a command", + "description": "if will execute the command CONDITION. If the condition's exit status\nis 0, the commands COMMANDS_TRUE will execute. If the exit status is\nnot 0 and else is given, COMMANDS_FALSE will be executed.\n\nYou can use and or or in the condition. See the second example below.\n\nThe exit status of the last foreground command to exit can always be\naccessed using the $status variable.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nThe following code will print foo.txt exists if the file foo.txt\nexists and is a regular file, otherwise it will print bar.txt exists\nif the file bar.txt exists and is a regular file, otherwise it will\nprint foo.txt and bar.txt do not exist.\n\n if test -f foo.txt\n echo foo.txt exists\n else if test -f bar.txt\n echo bar.txt exists\n else\n echo foo.txt and bar.txt do not exist\n end\n\nThe following code will print \"foo.txt exists and is readable\" if\nfoo.txt is a regular file and readable\n\n if test -f foo.txt\n and test -r foo.txt\n echo \"foo.txt exists and is readable\"\n end", + "args": "if CONDITION; COMMANDS_TRUE ...;\n[else if CONDITION2; COMMANDS_TRUE2 ...;]\n[else; COMMANDS_FALSE ...;]\nend" + }, + "jobs": { + "shortDescription": "print currently running jobs", + "description": "jobs prints a list of the currently running jobs and their status.\n\njobs accepts the following options:\n\n-c or --command\n Prints the command name for each process in jobs.\n\n-g or --group\n Only prints the group ID of each job.\n\n-l or --last\n Prints only the last job to be started.\n\n-p or --pid\n Prints the process ID for each process in all jobs.\n\n-q or --query\n Prints no output for evaluation of jobs by exit status only.\n For compatibility with old fish versions this is also --quiet\n (but this is deprecated).\n\n-h or --help\n Displays help about using this command.\n\nOn systems that support this feature, jobs will print the CPU usage\nof each job since the last command was executed. The CPU usage is\nexpressed as a percentage of full CPU activity. Note that on\nmultiprocessor systems, the total activity may be more than 100%.\n\nArguments of the form PID or %JOBID restrict the output to jobs with\nthe selected process identifiers or job numbers respectively.\n\nIf the output of jobs is redirected or if it is part of a command\nsubstitution, the column header that is usually printed is omitted,\nmaking it easier to parse.\n\nThe exit status of jobs is 0 if there are running background jobs and\n1 otherwise.\n\nEXAMPLE\njobs outputs a summary of the current jobs, such as two long-running\ntasks in this example:\n\n Job Group State Command\n 2 26012 running nc -l 55232 < /dev/random &\n 1 26011 running python tests/test_11.py &", + "args": "jobs [OPTIONS] [PID | %JOBID]" + }, + "math": { + "shortDescription": "perform mathematics calculations", + "description": "math performs mathematical calculations. It supports simple\noperations such as addition, subtraction, and so on, as well as\nfunctions like abs(), sqrt() and ln().\n\nBy default, the output shows up to 6 decimal places. To change the\nnumber of decimal places, use the --scale option, including --scale=0\nfor integer output. Trailing zeroes will always be trimmed.\n\nKeep in mind that parameter expansion happens before expressions are\nevaluated. This can be very useful in order to perform calculations\ninvolving shell variables or the output of command substitutions, but\nit also means that parenthesis (()) and the asterisk (*) glob\ncharacter have to be escaped or quoted. x can also be used to denote\nmultiplication, but it needs to be followed by whitespace to\ndistinguish it from hexadecimal numbers.\n\nParentheses for functions are optional - math sin pi prints 0.\nHowever, a comma will bind to the inner function, so math pow sin 3,\n5 is an error because it tries to give sin the arguments 3 and 5.\nWhen in doubt, use parentheses.\n\nmath ignores whitespace between arguments and takes its input as\nmultiple arguments (internally joined with a space), so math 2 +2 and\nmath \"2 + 2\" work the same. math 2 2 is an error.\n\nThe following options are available:\n\n-s N or --scale N\n Sets the scale of the result. N must be an integer or the\n word \"max\" for the maximum scale. A scale of zero causes\n results to be truncated, not rounded. Any non-integer\n component is thrown away. So 3/2 returns 1 rather than 2\n which 1.5 would normally round to. This is for compatibility\n with bc which was the basis for this command prior to fish\n 3.0.0. Scale values greater than zero causes the result to be\n rounded using the usual rules to the specified number of\n decimal places.\n\n-b BASE or --base BASE\n Sets the numeric base used for output (math always understands\n hexadecimal numbers as input). It currently understands \"hex\"\n or \"16\" for hexadecimal and \"octal\" or \"8\" for octal and\n implies a scale of 0 (other scales cause an error), so it will\n truncate the result down to an integer. This might change in\n the future. Hex numbers will be printed with a 0x prefix.\n Octal numbers will have a prefix of 0 but aren't understood by\n math as input.\n\n-h or --help\n Displays help about using this command.\n\nRETURN VALUES\nIf the expression is successfully evaluated and doesn't\nover/underflow or return NaN the return status is zero (success) else\none.\n\nSYNTAX\nmath knows some operators, constants, functions and can (obviously)\nread numbers.\n\nFor numbers, . is always the radix character regardless of locale -\n2.5, not 2,5. Scientific notation (10e5) and hexadecimal (0xFF) are\nalso available.\n\nmath allows you to use underscores as visual separators for digit\ngrouping. For example, you can write 1_000_000, 0x_89_AB_CD_EF, and\n1.234_567_e89.\n\nOPERATORS\nmath knows the following operators:\n\n+ for addition\n\n- for subtraction\n\n* or x for multiplication. * is the glob character and needs to be\n quoted or escaped, x needs to be followed by whitespace or it\n looks like 0x hexadecimal notation.\n\n/ for division\n\n^ for exponentiation\n\n% for modulo\n\n( or ) for grouping. These need to be quoted or escaped because ()\n denotes a command substitution.\n\nThey are all used in an infix manner - 5 + 2, not + 5 2.\n\nCONSTANTS\nmath knows the following constants:\n\ne Euler's number\n\npi π, you know this one. Half of Tau\n\ntau Equivalent to 2π, or the number of radians in a circle\n\nUse them without a leading $ - pi - 3 should be about 0.\n\nFUNCTIONS\nmath supports the following functions:\n\nabs the absolute value, with positive sign\n\nacos arc cosine\n\nasin arc sine\n\natan arc tangent\n\natan2 arc tangent of two variables\n\nbitand, bitor and bitxor\n perform bitwise operations. These will throw away any\n non-integer parts and interpret the rest as an int.\n\n Note: bitnot and bitnand don't exist. This is because numbers\n in math don't really have a width in terms of bits, and these\n operations necessarily care about leading zeroes.\n\n If you need to negate a specific number you can do it with an\n xor with a mask, e.g.:\n\n > math --base=hex bitxor 0x0F, 0xFF\n 0xF0\n\n > math --base=hex bitxor 0x2, 0x3\n # Here we mask with 0x3 == 0b111, so our number is 3 bits wide\n # Only the 1 bit isn't set.\n 0x1\n\nceil round number up to the nearest integer\n\ncos the cosine\n\ncosh hyperbolic cosine\n\nexp the base-e exponential function\n\nfac factorial - also known as x! (x * (x - 1) * (x - 2) * ... * 1)\n\nfloor round number down to the nearest integer\n\nln the base-e logarithm\n\nlog or log10\n the base-10 logarithm\n\nlog2 the base-2 logarithm\n\nmax returns the largest of the given numbers - this takes an\n arbitrary number of arguments (but at least one)\n\nmin returns the smallest of the given numbers - this takes an\n arbitrary number of arguments (but at least one)\n\nncr \"from n choose r\" combination function - how many subsets of\n size r can be taken from n (order doesn't matter)\n\nnpr the number of subsets of size r that can be taken from a set\n of n elements (including different order)\n\npow(x,y)\n returns x to the y (and can be written as x ^ y)\n\nround rounds to the nearest integer, away from 0\n\nsin the sine function\n\nsinh the hyperbolic sine\n\nsqrt the square root - (can also be written as x ^ 0.5)\n\ntan the tangent\n\ntanh the hyperbolic tangent\n\nAll of the trigonometric functions use radians (the pi-based scale,\nnot 360°).\n\nEXAMPLES\nmath 1+1 outputs 2.\n\nmath $status - 128 outputs the numerical exit status of the last\ncommand minus 128.\n\nmath 10 / 6 outputs 1.666667.\n\nmath -s0 10.0 / 6.0 outputs 1.\n\nmath -s3 10 / 6 outputs 1.666.\n\nmath \"sin(pi)\" outputs 0.\n\nmath 5 \\* 2 or math \"5 * 2\" or math 5 \"*\" 2 all output 10.\n\nmath 0xFF outputs 255, math 0 x 3 outputs 0 (because it computes 0\nmultiplied by 3).\n\nmath bitand 0xFE, 0x2e outputs 46.\n\nmath \"bitor(9,2)\" outputs 11.\n\nmath --base=hex 192 prints 0xc0.\n\nmath 'ncr(49,6)' prints 13983816 - that's the number of possible\npicks in 6-from-49 lotto.\n\nmath max 5,2,3,1 prints 5.\n\nCOMPATIBILITY NOTES\nFish 1.x and 2.x releases relied on the bc command for handling math\nexpressions. Starting with fish 3.0.0 fish uses the tinyexpr library\nand evaluates the expression without the involvement of any external\ncommands.\n\nYou don't need to use -- before the expression, even if it begins\nwith a minus sign which might otherwise be interpreted as an invalid\noption. If you do insert -- before the expression, it will cause\noption scanning to stop just like for every other command and it\nwon't be part of the expression.", + "args": "math [(-s | --scale) N] [(-b | --base) BASE] EXPRESSION ..." + }, + "not": { + "shortDescription": "negate the exit status of a job", + "description": "not negates the exit status of another command. If the exit status is\nzero, not returns 1. Otherwise, not returns 0.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nThe following code reports an error and exits if no file named spoon\ncan be found.\n\n if not test -f spoon\n echo There is no spoon\n exit 1\n end", + "args": "not COMMAND [OPTIONS ...]" + }, + "or": { + "shortDescription": "conditionally execute a command", + "description": "or is used to execute a command if the previous command was not\nsuccessful (returned a status of something other than 0).\n\nor statements may be used as part of the condition in an if or while\nblock.\n\nor does not change the current exit status itself, but the command it\nruns most likely will. The exit status of the last foreground command\nto exit can always be accessed using the $status variable.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nThe following code runs the make command to build a program. If the\nbuild succeeds, the program is installed. If either step fails, make\nclean is run, which removes the files created by the build process.\n\n make; and make install; or make clean\n\nSEE ALSO\n\n• and command", + "args": "COMMAND1; or COMMAND2" + }, + "path": { + "shortDescription": "manipulate and check paths", + "description": "path performs operations on paths.\n\nPATH arguments are taken from the command line unless standard input\nis connected to a pipe or a file, in which case they are read from\nstandard input, one PATH per line. It is an error to supply PATH\narguments on both the command line and on standard input.\n\nArguments starting with - are normally interpreted as switches; --\ncauses the following arguments not to be treated as switches even if\nthey begin with -. Switches and required arguments are recognized\nonly on the command line.\n\nWhen a path starts with -, path filter and path normalize will\nprepend ./ on output to avoid it being interpreted as an option\notherwise, so it's safe to pass path's output to other commands that\ncan handle relative paths.\n\nAll subcommands accept a -q or --quiet switch, which suppresses the\nusual output but exits with the documented status. In this case these\ncommands will quit early, without reading all of the available input.\n\nAll subcommands also accept a -Z or --null-out switch, which makes\nthem print output separated with NUL instead of newlines. This is for\nfurther processing, e.g. passing to another path, or xargs -0. This\nis not recommended when the output goes to the terminal or a command\nsubstitution.\n\nAll subcommands also accept a -z or --null-in switch, which makes\nthem accept arguments from stdin separated with NULL-bytes. Since\nUnix paths can't contain NULL, that makes it possible to handle all\npossible paths and read input from e.g. find -print0. If arguments\nare given on the commandline this has no effect. This should mostly\nbe unnecessary since path automatically starts splitting on NULL if\none appears in the first PATH_MAX bytes, PATH_MAX being the operating\nsystem's maximum length for a path plus a NULL byte.\n\nSome subcommands operate on the paths as strings and so work on\nnonexistent paths, while others need to access the paths themselves\nand so filter out nonexistent paths.\n\nThe following subcommands are available.\n\nBASENAME SUBCOMMAND\n\n path basename [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...]\n\npath basename returns the last path component of the given path, by\nremoving the directory prefix and removing trailing slashes. In other\nwords, it is the part that is not the dirname. For files you might\ncall it the \"filename\".\n\nIt returns 0 if there was a basename, i.e. if the path wasn't empty\nor just slashes.\n\n Examples\n\n > path basename ./foo.mp4\n foo.mp4\n\n > path basename ../banana\n banana\n\n > path basename /usr/bin/\n bin\n\n > path basename /usr/bin/*\n # This prints all files in /usr/bin/\n # A selection:\n cp\n fish\n grep\n rm\n\nDIRNAME SUBCOMMAND\n\n path dirname [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...]\n\npath dirname returns the dirname for the given path. This is the part\nbefore the last \"/\", discounting trailing slashes. In other words, it\nis the part that is not the basename (discounting superfluous\nslashes).\n\nIt returns 0 if there was a dirname, i.e. if the path wasn't empty or\njust slashes.\n\n Examples\n\n > path dirname ./foo.mp4\n .\n\n > path dirname ../banana\n ..\n\n > path dirname /usr/bin/\n /usr\n\nEXTENSION SUBCOMMAND\n\n path extension [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...]\n\npath extension returns the extension of the given path. This is the\npart after (and including) the last \".\", unless that \".\" followed a\n\"/\" or the basename is \".\" or \"..\", in which case there is no\nextension and an empty line is printed.\n\nIf the filename ends in a \".\", only a \".\" is printed.\n\nIt returns 0 if there was an extension.\n\n Examples\n\n > path extension ./foo.mp4\n .mp4\n\n > path extension ../banana\n # an empty line, status 1\n\n > path extension ~/.config\n # an empty line, status 1\n\n > path extension ~/.config.d\n .d\n\n > path extension ~/.config.\n .\n\n > set -l path (path change-extension '' ./foo.mp4)\n > set -l extension (path extension ./foo.mp4)\n > echo $path$extension\n # reconstructs the original path again.\n ./foo.mp4\n\nFILTER SUBCOMMAND\n\n path filter [-z | --null-in] [-Z | --null-out] [-q | --quiet] \\\n [-d] [-f] [-l] [-r] [-w] [-x] \\\n [-v | --invert] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH ...]\n\npath filter returns all of the given paths that match the given\nchecks. In all cases, the paths need to exist, nonexistent paths are\nalways filtered.\n\nThe available filters are:\n\n• -t or --type with the options: \"dir\", \"file\", \"link\", \"block\",\n \"char\", \"fifo\" and \"socket\", in which case the path needs to be a\n directory, file, link, block device, character device, named pipe\n or socket, respectively.\n\n• -d, -f and -l are short for --type=dir, --type=file and\n --type=link, respectively. There are no shortcuts for the other\n types.\n\n• -p or --perm with the options: \"read\", \"write\", and \"exec\", as well\n as \"suid\", \"sgid\", \"user\" (referring to the path owner) and \"group\"\n (referring to the path's group), in which case the path needs to\n have all of the given permissions for the current user.\n\n• -r, -w and -x are short for --perm=read, --perm=write and\n --perm=exec, respectively. There are no shortcuts for the other\n permissions.\n\nNote that the path needs to be any of the given types, but have all\nof the given permissions. This is because having a path that is both\nwritable and executable makes sense, but having a path that is both a\ndirectory and a file doesn't. Links will count as the type of the\nlinked-to file, so links to files count as files, links to\ndirectories count as directories.\n\nThe filter options can either be given as multiple options, or\ncomma-separated - path filter -t dir,file or path filter --type dir\n--type file are equivalent.\n\nWith --invert, the meaning of the filtering is inverted - any path\nthat wouldn't pass (including by not existing) passes, and any path\nthat would pass fails.\n\nWhen a path starts with -, path filter will prepend ./ to avoid it\nbeing interpreted as an option otherwise.\n\nIt returns 0 if at least one path passed the filter.\n\npath is is shorthand for path filter -q, i.e. just checking without\nproducing output, see The is subcommand.\n\n Examples\n\n > path filter /usr/bin /usr/argagagji\n # The (hopefully) nonexistent argagagji is filtered implicitly:\n /usr/bin\n\n > path filter --type file /usr/bin /usr/bin/fish\n # Only fish is a file\n /usr/bin/fish\n\n > path filter --type file,dir --perm exec,write /usr/bin/fish /home/me\n # fish is a file, which passes, and executable, which passes,\n # but probably not writable, which fails.\n #\n # $HOME is a directory and both writable and executable, typically.\n # So it passes.\n /home/me\n\n > path filter -fdxw /usr/bin/fish /home/me\n # This is the same as above: \"-f\" is \"--type=file\", \"-d\" is \"--type=dir\",\n # \"-x\" is short for \"--perm=exec\" and \"-w\" short for \"--perm=write\"!\n /home/me\n\n > path filter -fx $PATH/*\n # Prints all possible commands - the first entry of each name is what fish would execute!\n\nIS SUBCOMMAND\n\n path is [-z | --null-in] [-Z | --null-out] [-q | --quiet] \\\n [-d] [-f] [-l] [-r] [-w] [-x] \\\n [-v | --invert] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH ...]\n\npath is is short for path filter -q. It returns true if any of the\ngiven files passes the filter, but does not produce any output.\n\n--quiet can still be passed for compatibility but is redundant. The\noptions are the same as for path filter.\n\n Examples\n\n > path is /usr/bin /usr/argagagji\n # /usr/bin exists, so this returns a status of 0 (true). It prints nothing.\n > path is /usr/argagagji\n # /usr/argagagji does not, so this returns a status of 1 (false). It also prints nothing.\n > path is -fx /bin/sh\n # /bin/sh is usually an executable file, so this returns true.\n\nMTIME SUBCOMMAND\n\n path mtime [-z | --null-in] [-Z | --null-out] [-q | --quiet] [-R | --relative] [PATH ...]\n\npath mtime returns the last modification time (\"mtime\" in unix\njargon) of the given paths, in seconds since the unix epoch (the\nbeginning of the 1st of January 1970).\n\nWith --relative (or -R), it prints the number of seconds since the\nmodification time. It only reads the current time once at start, so\nin case multiple paths are given the times are all relative to the\nstart of path mtime -R running.\n\nIf you want to know if a file is newer or older than another file,\nconsider using test -nt instead. See the test documentation.\n\nIt returns 0 if reading mtime for any path succeeded.\n\n Examples\n\n > date +%s\n # This prints the current time as seconds since the epoch\n 1657217847\n\n > path mtime /etc/\n 1657213796\n\n > path mtime -R /etc/\n 4078\n # So /etc/ on this system was last modified a little over an hour ago\n\n # This is the same as\n > math (date +%s) - (path mtime /etc/)\n\nNORMALIZE SUBCOMMAND\n\n path normalize [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...]\n\npath normalize returns the normalized versions of all paths. That\nmeans it squashes duplicate \"/\" (except for two leading \"//\"),\ncollapses \"../\" with earlier components and removes \".\" components.\n\nUnlike realpath or path resolve, it does not make the paths absolute.\nIt also does not resolve any symlinks. As such it can operate on\nnon-existent paths.\n\nBecause it operates on paths as strings and doesn't resolve symlinks,\nit works sort of like pwd -L and cd. E.g. path normalize link/.. will\nreturn ., just like cd link; cd .. would return to the current\ndirectory. For a physical view of the filesystem, see path resolve.\n\nLeading \"./\" components are usually removed. But when a path starts\nwith -, path normalize will add it instead to avoid confusion with\noptions.\n\nIt returns 0 if any normalization was done, i.e. any given path\nwasn't in canonical form.\n\n Examples\n\n > path normalize /usr/bin//../../etc/fish\n # The \"//\" is squashed and the \"..\" components neutralize the components before\n /etc/fish\n\n > path normalize /bin//bash\n # The \"//\" is squashed, but /bin isn't resolved even if your system links it to /usr/bin.\n /bin/bash\n\n > path normalize ./my/subdirs/../sub2\n my/sub2\n\n > path normalize -- -/foo\n ./-/foo\n\nRESOLVE SUBCOMMAND\n\n path resolve [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...]\n\npath resolve returns the normalized, physical and absolute versions\nof all paths. That means it resolves symlinks and does what path\nnormalize does: it squashes duplicate \"/\", collapses \"../\" with\nearlier components and removes \".\" components. Then it turns that\npath into the absolute path starting from the filesystem root \"/\".\n\nIt is similar to realpath, as it creates the \"real\", canonical\nversion of the path. However, for paths that can't be resolved, e.g.\nif they don't exist or form a symlink loop, it will resolve as far as\nit can and normalize the rest.\n\nBecause it resolves symlinks, it works sort of like pwd -P. E.g. path\nresolve link/.. will return the parent directory of what the link\npoints to, just like cd link; cd (pwd -P)/.. would go to it. For a\nlogical view of the filesystem, see path normalize.\n\nIt returns 0 if any normalization or resolution was done, i.e. any\ngiven path wasn't in canonical form.\n\n Examples\n\n > path resolve /bin//sh\n # The \"//\" is squashed, and /bin is resolved if your system links it to /usr/bin.\n # sh here is bash (this is common on linux systems)\n /usr/bin/bash\n\n > path resolve /bin/foo///bar/../baz\n # Assuming /bin exists and is a symlink to /usr/bin, but /bin/foo doesn't.\n # This resolves the /bin/ and normalizes the nonexistent rest:\n /usr/bin/foo/baz\n\nCHANGE-EXTENSION SUBCOMMAND\n\n path change-extension [-z | --null-in] [-Z | --null-out] \\\n [-q | --quiet] EXTENSION [PATH ...]\n\npath change-extension returns the given paths, with their extension\nchanged to the given new extension. The extension is the part after\n(and including) the last \".\", unless that \".\" followed a \"/\" or the\nbasename is \".\" or \"..\", in which case there is no previous extension\nand the new one is simply added.\n\nIf the extension is empty, any previous extension is stripped, along\nwith the \".\". This is, of course, the inverse of path extension.\n\nOne leading dot on the extension is ignored, so \".mp3\" and \"mp3\" are\ntreated the same.\n\nIt returns 0 if it was given any paths.\n\n Examples\n\n > path change-extension mp4 ./foo.wmv\n ./foo.mp4\n\n > path change-extension .mp4 ./foo.wmv\n ./foo.mp4\n\n > path change-extension '' ../banana\n ../banana\n # but status 1, because there was no extension.\n\n > path change-extension '' ~/.config\n /home/alfa/.config\n # status 1\n\n > path change-extension '' ~/.config.d\n /home/alfa/.config\n # status 0\n\n > path change-extension '' ~/.config.\n /home/alfa/.config\n # status 0\n\nSORT SUBCOMMAND\n\n path sort [-z | --null-in] [-Z | --null-out] \\\n [-q | --quiet] [-r | --reverse] \\\n [--key=basename|dirname|path] [PATH ...]\n\npath sort returns the given paths in sorted order. They are sorted in\nthe same order as globs - alphabetically, but with runs of numerical\ndigits compared numerically.\n\nWith --reverse or -r the sort is reversed.\n\nWith --key= only the given part of the path is compared, e.g.\n--key=dirname causes only the dirname to be compared, --key=basename\nonly the basename and --key=path causes the entire path to be\ncompared (this is the default).\n\nWith --unique or -u the sort is deduplicated, meaning only the first\nof a run that have the same key is kept. So if you are sorting by\nbasename, then only the first of each basename is used.\n\nThe sort used is stable, so sorting first by basename and then by\ndirname works and causes the files to be grouped according to\ndirectory.\n\nIt currently returns 0 if it was given any paths.\n\n Examples\n\n > path sort 10-foo 2-bar\n 2-bar\n 10-foo\n\n > path sort --reverse 10-foo 2-bar\n 10-foo\n 2-bar\n\n > path sort --unique --key=basename $fish_function_path/*.fish\n # prints a list of all function files fish would use, sorted by name.\n\nCOMBINING PATH\npath is meant to be easy to combine with itself, other tools and\nfish.\n\nThis is why\n\n• path's output is automatically split by fish if it goes into a\n command substitution, so just doing (path ...) handles all paths,\n even those containing newlines, correctly\n\n• path has --null-in to handle null-delimited input (typically\n automatically detected!), and --null-out to pass on null-delimited\n output\n\nSome examples of combining path:\n\n # Expand all paths in the current directory, leave only executable files, and print their resolved path\n path filter -zZ -xf -- * | path resolve -z\n\n # The same thing, but using find (note -maxdepth needs to come first or find will scream)\n # (this also depends on your particular version of find)\n # Note the `-z` is unnecessary for any sensible version of find - if `path` sees a NULL,\n # it will split on NULL automatically.\n find . -maxdepth 1 -type f -executable -print0 | path resolve -z\n\n set -l paths (path filter -p exec $PATH/fish -Z | path resolve)", + "args": "path basename GENERAL_OPTIONS [PATH ...]\npath dirname GENERAL_OPTIONS [PATH ...]\npath extension GENERAL_OPTIONS [PATH ...]\npath filter GENERAL_OPTIONS [-v | --invert]\n [-d] [-f] [-l] [-r] [-w] [-x]\n [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH ...]\npath is GENERAL_OPTIONS [(-v | --invert)] [(-t | --type) TYPE]\n [-d] [-f] [-l] [-r] [-w] [-x]\n [(-p | --perm) PERMISSION] [PATH ...]\npath mtime GENERAL_OPTIONS [(-R | --relative)] [PATH ...]\npath normalize GENERAL_OPTIONS [PATH ...]\npath resolve GENERAL_OPTIONS [PATH ...]\npath change-extension GENERAL_OPTIONS EXTENSION [PATH ...]\npath sort GENERAL_OPTIONS [-r | --reverse]\n [-u | --unique] [--key=basename|dirname|path] [PATH ...]\n\nGENERAL_OPTIONS\n [-z | --null-in] [-Z | --null-out] [-q | --quiet]" + }, + "printf": { + "shortDescription": "Display formatted text", + "description": "The `printf` command formats and prints text according to a specified format string. Unlike `echo`, `printf` does not append a newline unless explicitly included in the format.", + "args": "FORMAT [ARGUMENT...]" + }, + "pwd": { + "shortDescription": "output the current working directory", + "description": "NOTE: This page documents the fish builtin pwd. To see the\ndocumentation on the pwd command you might have, use command man pwd.\n\npwd outputs (prints) the current working directory.\n\nThe following options are available:\n\n-L or --logical\n Output the logical working directory, without resolving\n symlinks (default behavior).\n\n-P or --physical\n Output the physical working directory, with symlinks resolved.\n\n-h or --help\n Displays help about using this command.\n\nSEE ALSO\nNavigate directories using the directory history or the directory\nstack", + "args": "pwd [-P | --physical]\npwd [-L | --logical]" + }, + "random": { + "shortDescription": "generate random number", + "description": "random generates a pseudo-random integer from a uniform distribution.\nThe range (inclusive) depends on the arguments.\n\nNo arguments indicate a range of 0 to 32767 (inclusive).\n\nIf one argument is specified, the internal engine will be seeded with\nthe argument for future invocations of random and no output will be\nproduced.\n\nTwo arguments indicate a range from START to END (both START and END\nincluded).\n\nThree arguments indicate a range from START to END with a spacing of\nSTEP between possible outputs.\n\nrandom choice will select one random item from the succeeding\narguments.\n\nThe -h or --help option displays help about using this command.\n\nNote that seeding the engine will NOT give the same result across\ndifferent systems.\n\nYou should not consider random cryptographically secure, or even\nstatistically accurate.\n\nEXAMPLE\nThe following code will count down from a random even number between\n10 and 20 to 1:\n\n for i in (seq (random 10 2 20) -1 1)\n echo $i\n end\n\nAnd this will open a random picture from any of the subdirectories:\n\n open (random choice **.jpg)\n\nOr, to only get even numbers from 2 to 20:\n\n random 2 2 20\n\nOr odd numbers from 1 to 3:\n\n random 1 2 3 # or 1 2 4", + "args": "random\nrandom SEED\nrandom START END\nrandom START STEP END\nrandom choice [ITEMS ...]" + }, + "read": { + "shortDescription": "read line of input into variables", + "description": "read reads from standard input and either writes the result back to\nstandard output (for use in command substitution), or stores the\nresult in one or more shell variables. By default, read reads a\nsingle line and splits it into variables on spaces or tabs.\nAlternatively, a null character or a maximum number of characters can\nbe used to terminate the input, and other delimiters can be given.\nUnlike other shells, there is no default variable (such as REPLY) for\nstoring the result - instead, it is printed on standard output.\n\nThe following options are available:\n\n-c CMD or --command CMD\n Sets the initial string in the interactive mode command buffer\n to CMD.\n\n-d or --delimiter DELIMITER\n Splits on DELIMITER. DELIMITER will be used as an entire\n string to split on, not a set of characters.\n\n-g or --global\n Makes the variables global.\n\n-s or --silent\n Masks characters written to the terminal, replacing them with\n asterisks. This is useful for reading things like passwords or\n other sensitive information.\n\n-f or --function\n Scopes the variable to the currently executing function. It is\n erased when the function ends.\n\n-l or --local\n Scopes the variable to the currently executing block. It is\n erased when the block ends. Outside of a block, this is the\n same as --function.\n\n-n or --nchars NCHARS\n Makes read return after reading NCHARS characters or the end\n of the line, whichever comes first.\n\n-p or --prompt PROMPT_CMD\n Uses the output of the shell command PROMPT_CMD as the prompt\n for the interactive mode. The default prompt command is\n set_color green; echo read; set_color normal; echo \"> \"\n\n-P or --prompt-str PROMPT_STR\n Uses the PROMPT_STR as the prompt for the interactive mode. It\n is equivalent to echo $PROMPT_STR and is provided solely to\n avoid the need to frame the prompt as a command. All special\n characters in the string are automatically escaped before\n being passed to the echo command.\n\n-R or --right-prompt RIGHT_PROMPT_CMD\n Uses the output of the shell command RIGHT_PROMPT_CMD as the\n right prompt for the interactive mode. There is no default\n right prompt command.\n\n-S or --shell\n Enables syntax highlighting, tab completions and command\n termination suitable for entering shellscript code in the\n interactive mode. NOTE: Prior to fish 3.0, the short opt for\n --shell was -s, but it has been changed for compatibility with\n bash's -s short opt for --silent.\n\n-t -or --tokenize\n Causes read to split the input into variables by the shell's\n tokenization rules. This means it will honor quotes and\n escaping. This option is of course incompatible with other\n options to control splitting like --delimiter and does not\n honor IFS (like fish's tokenizer). It saves the tokens in the\n manner they'd be passed to commands on the commandline, so\n e.g. a\\ b is stored as a b. Note that currently it leaves\n command substitutions intact along with the parentheses.\n\n-u or --unexport\n Prevents the variables from being exported to child processes\n (default behaviour).\n\n-U or --universal\n Causes the specified shell variable to be made universal.\n\n-x or --export\n Exports the variables to child processes.\n\n-a or --list\n Stores the result as a list in a single variable. This option\n is also available as --array for backwards compatibility.\n\n-z or --null\n Marks the end of the line with the NUL character, instead of\n newline. This also disables interactive mode.\n\n-L or --line\n Reads each line into successive variables, and stops after\n each variable has been filled. This cannot be combined with\n the --delimiter option.\n\nWithout the --line option, read reads a single line of input from\nstandard input, breaks it into tokens, and then assigns one token to\neach variable specified in VARIABLES. If there are more tokens than\nvariables, the complete remainder is assigned to the last variable.\n\nIf no option to determine how to split like --delimiter, --line or\n--tokenize is given, the variable IFS is used as a list of characters\nto split on. Relying on the use of IFS is deprecated and this\nbehaviour will be removed in future versions. The default value of\nIFS contains space, tab and newline characters. As a special case, if\nIFS is set to the empty string, each character of the input is\nconsidered a separate token.\n\nWith the --line option, read reads a line of input from standard\ninput into each provided variable, stopping when each variable has\nbeen filled. The line is not tokenized.\n\nIf no variable names are provided, read enters a special case that\nsimply provides redirection from standard input to standard output,\nuseful for command substitution. For instance, the fish shell command\nbelow can be used to read data that should be provided via a command\nline argument from the console instead of hardcoding it in the\ncommand itself, allowing the command to both be reused as-is in\nvarious contexts with different input values and preventing possibly\nsensitive text from being included in the shell history:\n\n mysql -uuser -p(read)\n\nWhen running in this mode, read does not split the input in any way\nand text is redirected to standard output without any further\nprocessing or manipulation.\n\nIf -a or --array is provided, only one variable name is allowed and\nthe tokens are stored as a list in this variable.\n\nSee the documentation for set for more details on the scoping rules\nfor variables.\n\nWhen read reaches the end-of-file (EOF) instead of the terminator,\nthe exit status is set to 1. Otherwise, it is set to 0.\n\nIn order to protect the shell from consuming too many system\nresources, read will only consume a maximum of 100 MiB (104857600\nbytes); if the terminator is not reached before this limit then\nVARIABLE is set to empty and the exit status is set to 122. This\nlimit can be altered with the fish_read_limit variable. If set to 0\n(zero), the limit is removed.\n\nEXAMPLE\nread has a few separate uses.\n\nThe following code stores the value 'hello' in the shell variable\nfoo.\n\n echo hello|read foo\n\nThe while command is a neat way to handle command output\nline-by-line:\n\n printf '%s\\n' line1 line2 line3 line4 | while read -l foo\n echo \"This is another line: $foo\"\n end\n\nDelimiters given via \"-d\" are taken as one string:\n\n echo a==b==c | read -d == -l a b c\n echo $a # a\n echo $b # b\n echo $c # c\n\n--tokenize honors quotes and escaping like the shell's argument\npassing:\n\n echo 'a\\ b' | read -t first second\n echo $first # outputs \"a b\", $second is empty\n\n echo 'a\"foo bar\"b (command echo wurst)*\" \"{a,b}' | read -lt -l a b c\n echo $a # outputs 'afoo barb' (without the quotes)\n echo $b # outputs '(command echo wurst)* {a,b}' (without the quotes)\n echo $c # nothing\n\nFor an example on interactive use, see Querying for user input.", + "args": "read [OPTIONS] [VARIABLE ...]" + }, + "realpath": { + "shortDescription": "Resolve and print the absolute path", + "description": "Convert each provided path to its absolute, canonical form by resolving symbolic links and relative path components.", + "args": "PATH..." + }, + "return": { + "shortDescription": "stop the current inner function", + "description": "return halts a currently running function. The exit status is set to\nN if it is given. If return is invoked outside of a function or dot\nscript it is equivalent to exit.\n\nIt is often added inside of a conditional block such as an if\nstatement or a switch statement to conditionally stop the executing\nfunction and return to the caller; it can also be used to specify the\nexit status of a function.\n\nIf at the top level of a script, it exits with the given status, like\nexit. If at the top level in an interactive session, it will set\nstatus, but not exit the shell.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nAn implementation of the false command as a fish function:\n\n function false\n return 1\n end", + "args": "return [N]" + }, + "set": { + "shortDescription": "display and change shell variables", + "description": "set manipulates shell variables.\n\nIf both NAME and VALUE are provided, set assigns any values to\nvariable NAME. Variables in fish are lists, multiple values are\nallowed. One or more variable INDEX can be specified including\nranges (not for all options.)\n\nIf no VALUE is given, the variable will be set to the empty list.\n\nIf set is ran without arguments, it prints the names and values of\nall shell variables in sorted order. Passing scope or export flags\nallows filtering this to only matching variables, so set --local\nwould only show local variables.\n\nWith --erase and optionally a scope flag set will erase the matching\nvariable (or the variable of that name in the smallest possible\nscope).\n\nWith --show, set will describe the given variable names, explaining\nhow they have been defined - in which scope with which values and\noptions.\n\nThe following options control variable scope:\n\n-U or --universal\n Sets a universal variable. The variable will be immediately\n available to all the user's fish instances on the machine, and\n will be persisted across restarts of the shell.\n\n-f or --function\n Sets a variable scoped to the executing function. It is\n erased when the function ends.\n\n-l or --local\n Sets a locally-scoped variable in this block. It is erased\n when the block ends. Outside of a block, this is the same as\n --function.\n\n-g or --global\n Sets a globally-scoped variable. Global variables are\n available to all functions running in the same shell. They\n can be modified or erased.\n\nThese options modify how variables operate:\n\n--export or -x\n Causes the specified shell variable to be exported to child\n processes (making it an \"environment variable\").\n\n--unexport or -u\n Causes the specified shell variable to NOT be exported to\n child processes.\n\n--path Treat specified variable as a path variable; variable will be\n split on colons (:) and will be displayed joined by colons\n when quoted (echo \"$PATH\") or exported.\n\n--unpath\n Causes variable to no longer be treated as a path variable.\n Note: variables ending in \"PATH\" are automatically path\n variables.\n\nFurther options:\n\n-a or --append NAME VALUE ...\n Appends VALUES to the current set of values for variable NAME.\n Can be used with --prepend to both append and prepend at the\n same time. This cannot be used when assigning to a variable\n slice.\n\n-p or --prepend NAME VALUE ...\n Prepends VALUES to the current set of values for variable\n NAME. This can be used with --append to both append and\n prepend at the same time. This cannot be used when assigning\n to a variable slice.\n\n-e or --erase NAME*[*INDEX]\n Causes the specified shell variables to be erased. Supports\n erasing from multiple scopes at once. Individual items in a\n variable at INDEX in brackets can be specified.\n\n-q or --query NAME*[*INDEX]\n Test if the specified variable names are defined. If an INDEX\n is provided, check for items at that slot. Does not output\n anything, but the shell status is set to the number of\n variables specified that were not defined, up to a maximum of\n 255. If no variable was given, it also returns 255.\n\n-n or --names\n List only the names of all defined variables, not their value.\n The names are guaranteed to be sorted.\n\n-S or --show\n Shows information about the given variables. If no variable\n names are given then all variables are shown in sorted order.\n It shows the scopes the given variables are set in, along with\n the values in each and whether or not it is exported. No\n other flags can be used with this option.\n\n-L or --long\n Do not abbreviate long values when printing set variables.\n\n-h or --help\n Displays help about using this command.\n\nIf a variable is set to more than one value, the variable will be a\nlist with the specified elements. If a variable is set to zero\nelements, it will become a list with zero elements.\n\nIf the variable name is one or more list elements, such as PATH[1 3\n7], only those list elements specified will be changed. If you\nspecify a negative index when expanding or assigning to a list\nvariable, the index will be calculated from the end of the list. For\nexample, the index -1 means the last index of a list.\n\nThe scoping rules when creating or updating a variable are:\n\n• Variables may be explicitly set as universal, global, function, or\n local. Variables with the same name but in a different scope will\n not be changed.\n\n• If the scope of a variable is not explicitly set but a variable by\n that name has been previously defined, the scope of the existing\n variable is used. If the variable is already defined in multiple\n scopes, the variable with the narrowest scope will be updated.\n\n• If a variable's scope is not explicitly set and there is no\n existing variable by that name, the variable will be local to the\n currently executing function. Note that this is different from\n using the -l or --local flag, in which case the variable will be\n local to the most-inner currently executing block, while without\n them the variable will be local to the function as a whole. If no\n function is executing, the variable will be set in the global\n scope.\n\nThe exporting rules when creating or updating a variable are\nidentical to the scoping rules for variables:\n\n• Variables may be explicitly set to either exported or not exported.\n When an exported variable goes out of scope, it is unexported.\n\n• If a variable is not explicitly set to be exported or not exported,\n but has been previously defined, the previous exporting rule for\n the variable is kept.\n\n• If a variable is not explicitly set to be either exported or\n unexported and has never before been defined, the variable will not\n be exported.\n\nIn query mode, the scope to be examined can be specified. Whether\nthe variable has to be a path variable or exported can also be\nspecified.\n\nIn erase mode, if variable indices are specified, only the specified\nslices of the list variable will be erased.\n\nset requires all options to come before any other arguments. For\nexample, set flags -l will have the effect of setting the value of\nthe variable flags to '-l', not making the variable local.\n\nEXIT STATUS\nIn assignment mode, set does not modify the exit status, but passes\nalong whatever status was set, including by command substitutions.\nThis allows capturing the output and exit status of a subcommand,\nlike in if set output (command).\n\nIn query mode, the exit status is the number of variables that were\nnot found.\n\nIn erase mode, set exits with a zero exit status in case of success,\nwith a non-zero exit status if the commandline was invalid, if any of\nthe variables did not exist or was a special read-only variable.\n\nEXAMPLES\nPrint all global, exported variables:\n\n > set -gx\n\nSet the value of the variable $foo to be 'hi'.:\n\n > set foo hi\n\nAppend the value \"there\" to the variable $foo:\n\n > set -a foo there\n\nRemove $smurf from the scope:\n\n > set -e smurf\n\nRemove $smurf from the global and universal scopes:\n\n > set -e -Ug smurf\n\nChange the fourth element of the $PATH list to ~/bin:\n\n > set PATH[4] ~/bin\n\nOutputs the path to Python if type -p returns true:\n\n if set python_path (type -p python)\n echo \"Python is at $python_path\"\n end\n\nSetting a variable doesn't modify $status; a command substitution\nstill will, though:\n\n > echo $status\n 0\n > false\n > set foo bar\n > echo $status\n 1\n > true\n > set foo banana (false)\n > echo $status\n 1\n\nVAR=VALUE command sets a variable for just one command, like other\nshells. This runs fish with a temporary home directory:\n\n > HOME=(mktemp -d) fish\n\n(which is essentially the same as):\n\n > begin; set -lx HOME (mktemp -d); fish; end\n\nNOTES\n\n• Fish versions prior to 3.0 supported the syntax set PATH[1] PATH[4]\n /bin /sbin, which worked like set PATH[1 4] /bin /sbin.", + "args": "set\nset (-f | --function) (-l | local) (-g | --global) (-U | --universal)\nset [-Uflg] NAME [VALUE ...]\nset [-Uflg] NAME[[INDEX ...]] [VALUE ...]\nset (-a | --append) [-flgU] NAME VALUE ...\nset (-q | --query) (-e | --erase) [-flgU] [NAME][[INDEX]] ...]\nset (-S | --show) [NAME ...]" + }, + "set_color": { + "shortDescription": "set the terminal color", + "description": "set_color is used to control the color and styling of text in the\nterminal. VALUE describes that styling. VALUE can be a reserved color\nname like red or an RGB color value given as 3 or 6 hexadecimal\ndigits (\"F27\" or \"FF2277\"). A special keyword normal resets text\nformatting to terminal defaults.\n\nValid colors include:\n\n • black, red, green, yellow, blue, magenta, cyan, white\n\n • brblack, brred, brgreen, bryellow, brblue, brmagenta, brcyan,\n brwhite\n\nThe br- (as in 'bright') forms are full-brightness variants of the 8\nstandard-brightness colors on many terminals. brblack has higher\nbrightness than black - towards gray.\n\nAn RGB value with three or six hex digits, such as A0FF33 or f2f can\nbe used. Fish will choose the closest supported color. A three digit\nvalue is equivalent to specifying each digit twice; e.g., set_color\n2BC is the same as set_color 22BBCC. Hexadecimal RGB values can be in\nlower or uppercase. Depending on the capabilities of your terminal\n(and the level of support set_color has for it) the actual color may\nbe approximated by a nearby matching reserved color name or set_color\nmay not have an effect on color.\n\nA second color may be given as a desired fallback color. e.g.\nset_color 124212 brblue will instruct set_color to use brblue if a\nterminal is not capable of the exact shade of grey desired. This is\nvery useful when an 8 or 16 color terminal might otherwise not use a\ncolor.\n\nThe following options are available:\n\n-b or --background COLOR\n Sets the background color.\n\n-c or --print-colors\n Prints the given colors or a colored list of the 16 named\n colors.\n\n-o or --bold\n Sets bold mode.\n\n-d or --dim\n Sets dim mode.\n\n-i or --italics\n Sets italics mode.\n\n-r or --reverse\n Sets reverse mode.\n\n-u or --underline\n Sets underlined mode.\n\n-h or --help\n Displays help about using this command.\n\nUsing the normal keyword will reset foreground, background, and all\nformatting back to default.\n\nNOTES\n\n1. Using the normal keyword will reset both background and foreground\n colors to whatever is the default for the terminal.\n\n2. Setting the background color only affects subsequently written\n characters. Fish provides no way to set the background color for\n the entire terminal window. Configuring the window background\n color (and other attributes such as its opacity) has to be done\n using whatever mechanisms the terminal provides. Look for a config\n option.\n\n3. Some terminals use the --bold escape sequence to switch to a\n brighter color set rather than increasing the weight of text.\n\n4. set_color works by printing sequences of characters to standard\n output. If used in command substitution or a pipe, these\n characters will also be captured. This may or may not be\n desirable. Checking the exit status of isatty stdout before using\n set_color can be useful to decide not to colorize output in a\n script.\n\nEXAMPLES\n\n set_color red; echo \"Roses are red\"\n set_color blue; echo \"Violets are blue\"\n set_color 62A; echo \"Eggplants are dark purple\"\n set_color normal; echo \"Normal is nice\" # Resets the background too\n\nTERMINAL CAPABILITY DETECTION\nFish uses some heuristics to determine what colors a terminal\nsupports to avoid sending sequences that it won't understand.\n\nIn particular it will:\n\n• Enable 256 colors if TERM contains \"xterm\", except for known\n exceptions (like MacOS 10.6 Terminal.app)\n\n• Enable 24-bit (\"true-color\") even if the $TERM entry only reports\n 256 colors. This includes modern xterm, VTE-based terminals like\n Gnome Terminal, Konsole and iTerm2.\n\n• Detect support for italics, dim, reverse and other modes.\n\nIf terminfo reports 256 color support for a terminal, 256 color\nsupport will always be enabled.\n\nTo force true-color support on or off, set fish_term24bit to \"1\" for\non and 0 for off - set -g fish_term24bit 1.\n\nTo debug color palette problems, tput colors may be useful to see the\nnumber of colors in terminfo for a terminal. Fish launched as fish -d\nterm_support will include diagnostic messages that indicate the color\nsupport mode in use.\n\nThe set_color command uses the terminfo database to look up how to\nchange terminal colors on whatever terminal is in use. Some systems\nhave old and incomplete terminfo databases, and lack color\ninformation for terminals that support it. Fish assumes that all\nterminals can use the [ANSI\nX3.64](https://en.wikipedia.org/wiki/ANSI_escape_code) escape\nsequences if the terminfo definition indicates a color below 16 is\nnot supported.", + "args": "set_color [OPTIONS] VALUE" + }, + "source": { + "shortDescription": "evaluate contents of file", + "description": "source evaluates the commands of the specified FILE in the current\nshell as a new block of code. This is different from starting a new\nprocess to perform the commands (i.e. fish < FILE) since the commands\nwill be evaluated by the current shell, which means that changes in\nshell variables will affect the current shell. If additional\narguments are specified after the file name, they will be inserted\ninto the argv variable. The argv variable will not include the name\nof the sourced file.\n\nfish will search the working directory to resolve relative paths but\nwill not search PATH .\n\nIf no file is specified and stdin is not the terminal, or if the file\nname - is used, stdin will be read.\n\nThe exit status of source is the exit status of the last job to\nexecute. If something goes wrong while opening or reading the file,\nsource exits with a non-zero status.\n\n. (a single period) is an alias for the source command. The use of .\nis deprecated in favour of source, and . will be removed in a future\nversion of fish.\n\nsource creates a new local scope; set --local within a sourced block\nwill not affect variables in the enclosing scope.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\n\n source ~/.config/fish/config.fish\n # Causes fish to re-read its initialization file.\n\nCAVEATS\nIn fish versions prior to 2.3.0, the argv variable would have a\nsingle element (the name of the sourced file) if no arguments are\npresent. Otherwise, it would contain arguments without the name of\nthe sourced file. That behavior was very confusing and unlike other\nshells such as bash and zsh.", + "args": "source FILE [ARGUMENTS ...]\nSOMECOMMAND | source" + }, + "status": { + "shortDescription": "query fish runtime information", + "description": "With no arguments, status displays a summary of the current login and\njob control status of the shell.\n\nThe following operations (subcommands) are available:\n\nis-command-substitution, -c or --is-command-substitution\n Returns 0 if fish is currently executing a command\n substitution.\n\nis-block, -b or --is-block\n Returns 0 if fish is currently executing a block of code.\n\nis-breakpoint\n Returns 0 if fish is currently showing a prompt in the context\n of a breakpoint command. See also the fish_breakpoint_prompt\n function.\n\nis-interactive, -i or --is-interactive\n Returns 0 if fish is interactive - that is, connected to a\n keyboard.\n\nis-login, -l or --is-login\n Returns 0 if fish is a login shell - that is, if fish should\n perform login tasks such as setting up PATH.\n\nis-full-job-control or --is-full-job-control\n Returns 0 if full job control is enabled.\n\nis-interactive-job-control or --is-interactive-job-control\n Returns 0 if interactive job control is enabled.\n\nis-no-job-control or --is-no-job-control\n Returns 0 if no job control is enabled.\n\ncurrent-command\n Prints the name of the currently-running function or command,\n like the deprecated variable.\n\ncurrent-commandline\n Prints the entirety of the currently-running commandline,\n inclusive of all jobs and operators.\n\nfilename, current-filename, -f or --current-filename\n Prints the filename of the currently-running script. If the\n current script was called via a symlink, this will return the\n symlink. If the current script was received by piping into\n source, then this will return -.\n\nbasename\n Prints just the filename of the running script, without any\n path components before.\n\ndirname\n Prints just the path to the running script, without the actual\n filename itself. This can be relative to PWD (including just\n \".\"), depending on how the script was called. This is the same\n as passing the filename to dirname(3). It's useful if you want\n to use other files in the current script's directory or\n similar.\n\nfish-path\n Prints the absolute path to the currently executing instance\n of fish. This is a best-effort attempt and the exact output is\n down to what the platform gives fish. In some cases you might\n only get \"fish\".\n\nfunction or current-function\n Prints the name of the currently called function if able, when\n missing displays \"Not a function\" (or equivalent translated\n string).\n\nline-number, current-line-number, -n or --current-line-number\n Prints the line number of the currently running script.\n\nstack-trace, print-stack-trace, -t or --print-stack-trace\n Prints a stack trace of all function calls on the call stack.\n\njob-control, -j or --job-control CONTROL_TYPE\n Sets the job control type to CONTROL_TYPE, which can be none,\n full, or interactive.\n\nfeatures\n Lists all available feature flags.\n\ntest-feature FEATURE\n Returns 0 when FEATURE is enabled, 1 if it is disabled, and 2\n if it is not recognized.\n\nNOTES\nFor backwards compatibility most subcommands can also be specified as\na long or short option. For example, rather than status is-login you\ncan type status --is-login. The flag forms are deprecated and may be\nremoved in a future release (but not before fish 4.0).\n\nYou can only specify one subcommand per invocation even if you use\nthe flag form of the subcommand.", + "args": "status\nstatus is-login\nstatus is-interactive\nstatus is-block\nstatus is-breakpoint\nstatus is-command-substitution\nstatus is-no-job-control\nstatus is-full-job-control\nstatus is-interactive-job-control\nstatus current-command\nstatus current-commandline\nstatus filename\nstatus basename\nstatus dirname\nstatus fish-path\nstatus function\nstatus line-number\nstatus stack-trace\nstatus job-control CONTROL_TYPE\nstatus features\nstatus test-feature FEATURE" + }, + "string": { + "shortDescription": "manipulate strings", + "description": "string performs operations on strings.\n\nSTRING arguments are taken from the command line unless standard\ninput is connected to a pipe or a file, in which case they are read\nfrom standard input, one STRING per line. It is an error to supply\nSTRING arguments on the command line and on standard input.\n\nArguments beginning with - are normally interpreted as switches; --\ncauses the following arguments not to be treated as switches even if\nthey begin with -. Switches and required arguments are recognized\nonly on the command line.\n\nMost subcommands accept a -q or --quiet switch, which suppresses the\nusual output but exits with the documented status. In this case these\ncommands will quit early, without reading all of the available input.\n\nThe following subcommands are available.\n\nCOLLECT SUBCOMMAND\nstring collect [-a | --allow-empty] [-N | --no-trim-newlines] [STRING ...]\n\nstring collect collects its input into a single output argument,\nwithout splitting the output when used in a command substitution.\nThis is useful when trying to collect multiline output from another\ncommand into a variable. Exit status: 0 if any output argument is\nnon-empty, or 1 otherwise.\n\nA command like echo (cmd | string collect) is mostly equivalent to a\nquoted command substitution (echo \"$(cmd)\"). The main difference is\nthat the former evaluates to zero or one elements whereas the quoted\ncommand substitution always evaluates to one element due to string\ninterpolation.\n\nIf invoked with multiple arguments instead of input, string collect\npreserves each argument separately, where the number of output\narguments is equal to the number of arguments given to string\ncollect.\n\nAny trailing newlines on the input are trimmed, just as with \"$(cmd)\"\nsubstitution. Use --no-trim-newlines to disable this behavior, which\nmay be useful when running a command such as set contents (cat\nfilename | string collect -N).\n\nWith --allow-empty, string collect always prints one (empty)\nargument. This can be used to prevent an argument from disappearing.\n\n Examples\n\n > echo \"zero $(echo one\\ntwo\\nthree) four\"\n zero one\n two\n three four\n\n > echo \\\"(echo one\\ntwo\\nthree | string collect)\\\"\n \"one\n two\n three\"\n\n > echo \\\"(echo one\\ntwo\\nthree | string collect -N)\\\"\n \"one\n two\n three\n \"\n\n > echo foo(true | string collect --allow-empty)bar\n foobar\n\nESCAPE AND UNESCAPE SUBCOMMANDS\nstring escape [-n | --no-quoted] [--style=] [STRING ...]\nstring unescape [--style=] [STRING ...]\n\nstring escape escapes each STRING in one of three ways. The first is\n--style=script. This is the default. It alters the string such that\nit can be passed back to eval to produce the original argument again.\nBy default, all special characters are escaped, and quotes are used\nto simplify the output when possible. If -n or --no-quoted is given,\nthe simplifying quoted format is not used. Exit status: 0 if at least\none string was escaped, or 1 otherwise.\n\n--style=var ensures the string can be used as a variable name by hex\nencoding any non-alphanumeric characters. The string is first\nconverted to UTF-8 before being encoded.\n\n--style=url ensures the string can be used as a URL by hex encoding\nany character which is not legal in a URL. The string is first\nconverted to UTF-8 before being encoded.\n\n--style=regex escapes an input string for literal matching within a\nregex expression. The string is first converted to UTF-8 before being\nencoded.\n\nstring unescape performs the inverse of the string escape command. If\nthe string to be unescaped is not properly formatted it is ignored.\nFor example, doing string unescape --style=var (string escape\n--style=var $str) will return the original string. There is no\nsupport for unescaping --style=regex.\n\n Examples\n\n > echo \\x07 | string escape\n \\cg\n\n > string escape --style=var 'a1 b2'\\u6161\n a1_20_b2_E6_85_A1\n\nJOIN AND JOIN0 SUBCOMMANDS\nstring join [-q | --quiet] SEP [STRING ...]\nstring join0 [-q | --quiet] [STRING ...]\n\nstring join joins its STRING arguments into a single string separated\nby SEP, which can be an empty string. Exit status: 0 if at least one\njoin was performed, or 1 otherwise. If -n or --no-empty is specified,\nempty strings are excluded from consideration (e.g. string join -n +\na b \"\" c would expand to a+b+c not a+b++c).\n\nstring join0 joins its STRING arguments into a single string\nseparated by the zero byte (NUL), and adds a trailing NUL. This is\nmost useful in conjunction with tools that accept NUL-delimited\ninput, such as sort -z. Exit status: 0 if at least one join was\nperformed, or 1 otherwise.\n\nBecause Unix uses NUL as the string terminator, passing the output of\nstring join0 as an argument to a command (via a command substitution)\nwon't actually work. Fish will pass the correct bytes along, but the\ncommand won't be able to tell where the argument ends. This is a\nlimitation of Unix' argument passing.\n\n Examples\n\n > seq 3 | string join ...\n 1...2...3\n\n # Give a list of NUL-separated filenames to du (this is a GNU extension)\n > string join0 file1 file2 file\\nwith\\nmultiple\\nlines | du --files0-from=-\n\n # Just put the strings together without a separator\n > string join '' a b c\n abc\n\nLENGTH SUBCOMMAND\nstring length [-q | --quiet] [-V | --visible] [STRING ...]\n\nstring length reports the length of each string argument in\ncharacters. Exit status: 0 if at least one non-empty STRING was\ngiven, or 1 otherwise.\n\nWith -V or --visible, it uses the visible width of the arguments.\nThat means it will discount escape sequences fish knows about,\naccount for $fish_emoji_width and $fish_ambiguous_width. It will also\ncount each line (separated by \\n) on its own, and with a carriage\nreturn (\\r) count only the widest stretch on a line. The intent is to\nmeasure the number of columns the STRING would occupy in the current\nterminal.\n\n Examples\n\n > string length 'hello, world'\n 12\n\n > set str foo\n > string length -q $str; echo $status\n 0\n # Equivalent to test -n \"$str\"\n\n > string length --visible (set_color red)foobar\n # the set_color is discounted, so this is the width of \"foobar\"\n 6\n\n > string length --visible 🐟🐟🐟🐟\n # depending on $fish_emoji_width, this is either 4 or 8\n # in new terminals it should be\n 8\n\n > string length --visible abcdef\\r123\n # this displays as \"123def\", so the width is 6\n 6\n\n > string length --visible a\\nbc\n # counts \"a\" and \"bc\" as separate lines, so it prints width for each\n 1\n 2\n\nLOWER SUBCOMMAND\nstring lower [-q | --quiet] [STRING ...]\n\nstring lower converts each string argument to lowercase. Exit status:\n0 if at least one string was converted to lowercase, else 1. This\nmeans that in conjunction with the -q flag you can readily test\nwhether a string is already lowercase.\n\nMATCH SUBCOMMAND\nstring match [-a | --all] [-e | --entire] [-i | --ignore-case]\n [-g | --groups-only] [-r | --regex] [-n | --index]\n [-q | --quiet] [-v | --invert]\n PATTERN [STRING ...]\n\nstring match tests each STRING against PATTERN and prints matching\nsubstrings. Only the first match for each STRING is reported unless\n-a or --all is given, in which case all matches are reported.\n\nIf you specify the -e or --entire then each matching string is\nprinted including any prefix or suffix not matched by the pattern\n(equivalent to grep without the -o flag). You can, obviously, achieve\nthe same result by prepending and appending * or .* depending on\nwhether or not you have specified the --regex flag. The --entire flag\nis simply a way to avoid having to complicate the pattern in that\nfashion and make the intent of the string match clearer. Without\n--entire and --regex, a PATTERN will need to match the entire STRING\nbefore it will be reported.\n\nMatching can be made case-insensitive with --ignore-case or -i.\n\nIf --groups-only or -g is given, only the capturing groups will be\nreported - meaning the full match will be skipped. This is\nincompatible with --entire and --invert, and requires --regex. It is\nuseful as a simple cutting tool instead of string replace, so you can\nsimply choose \"this part\" of a string.\n\nIf --index or -n is given, each match is reported as a 1-based start\nposition and a length. By default, PATTERN is interpreted as a glob\npattern matched against each entire STRING argument. A glob pattern\nis only considered a valid match if it matches the entire STRING.\n\nIf --regex or -r is given, PATTERN is interpreted as a\nPerl-compatible regular expression, which does not have to match the\nentire STRING. For a regular expression containing capturing groups,\nmultiple items will be reported for each match, one for the entire\nmatch and one for each capturing group. With this, only the matching\npart of the STRING will be reported, unless --entire is given.\n\nWhen matching via regular expressions, string match automatically\nsets variables for all named capturing groups ((?expression)).\nIt will create a variable with the name of the group, in the default\nscope, for each named capturing group, and set it to the value of the\ncapturing group in the first matched argument. If a named capture\ngroup matched an empty string, the variable will be set to the empty\nstring (like set var \"\"). If it did not match, the variable will be\nset to nothing (like set var). When --regex is used with --all, this\nbehavior changes. Each named variable will contain a list of matches,\nwith the first match contained in the first element, the second match\nin the second, and so on. If the group was empty or did not match,\nthe corresponding element will be an empty string.\n\nIf --invert or -v is used the selected lines will be only those which\ndo not match the given glob pattern or regular expression.\n\nExit status: 0 if at least one match was found, or 1 otherwise.\n\n Match Glob Examples\n\n > string match '?' a\n a\n\n > string match 'a*b' axxb\n axxb\n\n > string match -i 'a??B' Axxb\n Axxb\n\n > string match -- '-*' -h foo --version bar\n # To match things that look like options, we need a `--`\n # to tell string its options end there.\n -h\n --version\n\n > echo 'ok?' | string match '*\\?'\n ok?\n\n # Note that only the second STRING will match here.\n > string match 'foo' 'foo1' 'foo' 'foo2'\n foo\n\n > string match -e 'foo' 'foo1' 'foo' 'foo2'\n foo1\n foo\n foo2\n\n > string match 'foo?' 'foo1' 'foo' 'foo2'\n foo1\n foo2\n\n Match Regex Examples\n\n > string match -r 'cat|dog|fish' 'nice dog'\n dog\n\n > string match -r -v \"c.*[12]\" {cat,dog}(seq 1 4)\n dog1\n dog2\n cat3\n dog3\n cat4\n dog4\n\n > string match -r -- '-.*' -h foo --version bar\n # To match things that look like options, we need a `--`\n # to tell string its options end there.\n -h\n --version\n\n > string match -r '(\\d\\d?):(\\d\\d):(\\d\\d)' 2:34:56\n 2:34:56\n 2\n 34\n 56\n\n > string match -r '^(\\w{2,4})\\1$' papa mud murmur\n papa\n pa\n murmur\n mur\n\n > string match -r -a -n at ratatat\n 2 2\n 4 2\n 6 2\n\n > string match -r -i '0x[0-9a-f]{1,8}' 'int magic = 0xBadC0de;'\n 0xBadC0de\n\n > echo $version\n 3.1.2-1575-ga2ff32d90\n > string match -rq '(?\\d+).(?\\d+).(?\\d+)' -- $version\n > echo \"You are using fish $major!\"\n You are using fish 3!\n\n > string match -raq ' *(?[^.!?]+)(?[.!?])?' \"hello, friend. goodbye\"\n > printf \"%s\\n\" -- $sentence\n hello, friend\n goodbye\n > printf \"%s\\n\" -- $punctuation\n .\n\n > string match -rq '(?hello)' 'hi'\n > count $word\n 0\n\nPAD AND SHORTEN SUBCOMMANDS\nstring pad [-r | --right] [(-c | --char) CHAR] [(-w | --width) INTEGER]\n [STRING ...]\n\nstring pad extends each STRING to the given visible width by adding\nCHAR to the left. That means the width of all visible characters\nadded together, excluding escape sequences and accounting for\nfish_emoji_width and fish_ambiguous_width. It is the amount of\ncolumns in a terminal the STRING occupies.\n\nThe escape sequences reflect what fish knows about, and how it\ncomputes its output. Your terminal might support more escapes, or not\nsupport escape sequences that fish knows about.\n\nIf -r or --right is given, add the padding after a string.\n\nIf -c or --char is given, pad with CHAR instead of whitespace.\n\nThe output is padded to the maximum width of all input strings. If -w\nor --width is given, use at least that.\n\n > string pad -w 10 abc abcdef\n abc\n abcdef\n\n > string pad --right --char=🐟 \"fish are pretty\" \"rich. \"\n fish are pretty\n rich. 🐟🐟🐟🐟\n\n > string pad -w$COLUMNS (date)\n # Prints the current time on the right edge of the screen.\n\nSEE ALSO\n\n• The printf command can do simple padding, for example printf %10s\\n\n works like string pad -w10.\n\n• string length with the --visible option can be used to show what\n fish thinks the width is.\nstring shorten [(-c | --char) CHARS] [(-m | --max) INTEGER]\n [-N | --no-newline] [-l | --left] [-q | --quiet] [STRING ...]\n\nstring shorten truncates each STRING to the given visible width and\nadds an ellipsis to indicate it. \"Visible width\" means the width of\nall visible characters added together, excluding escape sequences and\naccounting for fish_emoji_width and fish_ambiguous_width. It is the\namount of columns in a terminal the STRING occupies.\n\nThe escape sequences reflect what fish knows about, and how it\ncomputes its output. Your terminal might support more escapes, or not\nsupport escape sequences that fish knows about.\n\nIf -m or --max is given, truncate at the given width. Otherwise, the\nlowest non-zero width of all input strings is used. A max of 0 means\nno shortening takes place, all STRINGs are printed as-is.\n\nIf -N or --no-newline is given, only the first line (or last line\nwith --left) of each STRING is used, and an ellipsis is added if it\nwas multiline. This only works for STRINGs being given as arguments,\nmultiple lines given on stdin will be interpreted as separate STRINGs\ninstead.\n\nIf -c or --char is given, add CHAR instead of an ellipsis. This can\nalso be empty or more than one character.\n\nIf -l or --left is given, remove text from the left on instead, so\nthis prints the longest suffix of the string that fits. With\n--no-newline, this will take from the last line instead of the first.\n\nIf -q or --quiet is given, string shorten only runs for the return\nvalue - if anything would be shortened, it returns 0, else 1.\n\nThe default ellipsis is …. If fish thinks your system is incapable\nbecause of your locale, it will use ... instead.\n\nThe return value is 0 if any shortening occured, 1 otherwise.\n\n > string shorten foo foobar\n # No width was given, we infer, and \"foo\" is the shortest.\n foo\n fo…\n\n > string shorten --char=\"...\" foo foobar\n # The target width is 3 because of \"foo\",\n # and our ellipsis is 3 too, so we can't really show anything.\n # This is the default ellipsis if your locale doesn't allow \"…\".\n foo\n ...\n\n > string shorten --char=\"\" --max 4 abcdef 123456\n # Leaving the char empty makes us not add an ellipsis\n # So this truncates at 4 columns:\n abcd\n 1234\n\n > touch \"a multiline\"\\n\"file\"\n > for file in *; string shorten -N -- $file; end\n # Shorten the multiline file so we only show one line per file:\n a multiline…\n\n > ss -p | string shorten -m$COLUMNS -c \"\"\n # `ss` from Linux' iproute2 shows socket information, but prints extremely long lines.\n # This shortens input so it fits on the screen without overflowing lines.\n\n > git branch | string match -rg '^\\* (.*)' | string shorten -m20\n # Take the current git branch and shorten it at 20 columns.\n # Here the branch is \"builtin-path-with-expand\"\n builtin-path-with-e…\n\n > git branch | string match -rg '^\\* (.*)' | string shorten -m20 --left\n # Taking 20 columns from the right instead:\n …in-path-with-expand\n\nSEE ALSO\n\n• string's pad subcommand does the inverse of this command, adding\n padding to a specific width instead.\n\n• The printf command can do simple padding, for example printf %10s\\n\n works like string pad -w10.\n\n• string length with the --visible option can be used to show what\n fish thinks the width is.\n\nREPEAT SUBCOMMAND\nstring repeat [(-n | --count) COUNT] [(-m | --max) MAX] [-N | --no-newline]\n [-q | --quiet] [STRING ...]\n\nstring repeat repeats the STRING -n or --count times. The -m or --max\noption will limit the number of outputted characters (excluding the\nnewline). This option can be used by itself or in conjunction with\n--count. If both --count and --max are present, max char will be\noutputed unless the final repeated string size is less than max, in\nthat case, the string will repeat until count has been reached. Both\n--count and --max will accept a number greater than or equal to zero,\nin the case of zero, nothing will be outputed. If -N or --no-newline\nis given, the output won't contain a newline character at the end.\nExit status: 0 if yielded string is not empty, 1 otherwise.\n\n Examples\n Repeat Examples\n\n > string repeat -n 2 'foo '\n foo foo\n\n > echo foo | string repeat -n 2\n foofoo\n\n > string repeat -n 2 -m 5 'foo'\n foofo\n\n > string repeat -m 5 'foo'\n foofo\n\nREPLACE SUBCOMMAND\nstring replace [-a | --all] [-f | --filter] [-i | --ignore-case]\n [-r | --regex] [-q | --quiet] PATTERN REPLACEMENT [STRING ...]\n\nstring replace is similar to string match but replaces\nnon-overlapping matching substrings with a replacement string and\nprints the result. By default, PATTERN is treated as a literal\nsubstring to be matched.\n\nIf -r or --regex is given, PATTERN is interpreted as a\nPerl-compatible regular expression, and REPLACEMENT can contain\nC-style escape sequences like t as well as references to capturing\ngroups by number or name as $n or ${n}.\n\nIf you specify the -f or --filter flag then each input string is\nprinted only if a replacement was done. This is useful where you\nwould otherwise use this idiom: a_cmd | string match pattern | string\nreplace pattern new_pattern. You can instead just write a_cmd |\nstring replace --filter pattern new_pattern.\n\nExit status: 0 if at least one replacement was performed, or 1\notherwise.\n\n Replace Literal Examples\n\n > string replace is was 'blue is my favorite'\n blue was my favorite\n\n > string replace 3rd last 1st 2nd 3rd\n 1st\n 2nd\n last\n\n > string replace -a ' ' 'spaces to underscores'\n spaces_to_underscores\n\n Replace Regex Examples\n\n > string replace -r -a '[^\\d.]+' ' ' '0 one two 3.14 four 5x'\n 0 3.14 5\n\n > string replace -r '(\\w+)\\s+(\\w+)' '$2 $1 $$' 'left right'\n right left $\n\n > string replace -r '\\s*newline\\s*' '\\n' 'put a newline here'\n put a\n here\n\nSPLIT AND SPLIT0 SUBCOMMANDS\nstring split [(-f | --fields) FIELDS] [(-m | --max) MAX] [-n | --no-empty]\n [-q | --quiet] [-r | --right] SEP [STRING ...]\nstring split0 [(-f | --fields) FIELDS] [(-m | --max) MAX] [-n | --no-empty]\n [-q | --quiet] [-r | --right] [STRING ...]\n\nstring split splits each STRING on the separator SEP, which can be an\nempty string. If -m or --max is specified, at most MAX splits are\ndone on each STRING. If -r or --right is given, splitting is\nperformed right-to-left. This is useful in combination with -m or\n--max. With -n or --no-empty, empty results are excluded from\nconsideration (e.g. hello\\n\\nworld would expand to two strings and\nnot three). Exit status: 0 if at least one split was performed, or 1\notherwise.\n\nUse -f or --fields to print out specific fields. FIELDS is a\ncomma-separated string of field numbers and/or spans. Each field is\none-indexed, and will be printed on separate lines. If a given field\ndoes not exist, then the command exits with status 1 and does not\nprint anything, unless --allow-empty is used.\n\nSee also the --delimiter option of the read command.\n\nstring split0 splits each STRING on the zero byte (NUL). Options are\nthe same as string split except that no separator is given.\n\nsplit0 has the important property that its output is not further\nsplit when used in a command substitution, allowing for the command\nsubstitution to produce elements containing newlines. This is most\nuseful when used with Unix tools that produce zero bytes, such as\nfind -print0 or sort -z. See split0 examples below.\n\n Examples\n\n > string split . example.com\n example\n com\n\n > string split -r -m1 / /usr/local/bin/fish\n /usr/local/bin\n fish\n\n > string split '' abc\n a\n b\n c\n\n > string split --allow-empty -f1,3-4,5 '' abcd\n a\n c\n d\n\n NUL Delimited Examples\n\n > # Count files in a directory, without being confused by newlines.\n > count (find . -print0 | string split0)\n 42\n\n > # Sort a list of elements which may contain newlines\n > set foo beta alpha\\ngamma\n > set foo (string join0 $foo | sort -z | string split0)\n > string escape $foo[1]\n alpha\\ngamma\n\nSUB SUBCOMMAND\nstring sub [(-s | --start) START] [(-e | --end) END] [(-l | --length) LENGTH]\n [-q | --quiet] [STRING ...]\n\nstring sub prints a substring of each string argument. The start/end\nof the substring can be specified with -s/-e or --start/--end\nfollowed by a 1-based index value. Positive index values are relative\nto the start of the string and negative index values are relative to\nthe end of the string. The default start value is 1. The length of\nthe substring can be specified with -l or --length. If the length or\nend is not specified, the substring continues to the end of each\nSTRING. Exit status: 0 if at least one substring operation was\nperformed, 1 otherwise. --length is mutually exclusive with --end.\n\n Examples\n\n > string sub --length 2 abcde\n ab\n\n > string sub -s 2 -l 2 abcde\n bc\n\n > string sub --start=-2 abcde\n de\n\n > string sub --end=3 abcde\n abc\n\n > string sub -e -1 abcde\n abcd\n\n > string sub -s 2 -e -1 abcde\n bcd\n\n > string sub -s -3 -e -2 abcde\n c\n\nTRIM SUBCOMMAND\nstring trim [-l | --left] [-r | --right] [(-c | --chars) CHARS]\n [-q | --quiet] [STRING ...]\n\nstring trim removes leading and trailing whitespace from each STRING.\nIf -l or --left is given, only leading whitespace is removed. If -r\nor --right is given, only trailing whitespace is trimmed. The -c or\n--chars switch causes the characters in CHARS to be removed instead\nof whitespace. Exit status: 0 if at least one character was trimmed,\nor 1 otherwise.\n\n Examples\n\n > string trim ' abc '\n abc\n\n > string trim --right --chars=yz xyzzy zany\n x\n zan\n\nUPPER SUBCOMMAND\nstring upper [-q | --quiet] [STRING ...]\n\nstring upper converts each string argument to uppercase. Exit status:\n0 if at least one string was converted to uppercase, else 1. This\nmeans that in conjunction with the -q flag you can readily test\nwhether a string is already uppercase.\n\nREGULAR EXPRESSIONS\nBoth the match and replace subcommand support regular expressions\nwhen used with the -r or --regex option. The dialect is that of\nPCRE2.\n\nIn general, special characters are special by default, so a+ matches\none or more \"a\"s, while a\\+ matches an \"a\" and then a \"+\". (a+)\nmatches one or more \"a\"s in a capturing group ((?:XXXX) denotes a\nnon-capturing group). For the replacement parameter of replace, $n\nrefers to the n-th group of the match. In the match parameter, \\n\n(e.g. \\1) refers back to groups.\n\nSome features include repetitions:\n\n• * refers to 0 or more repetitions of the previous expression\n\n• + 1 or more\n\n• ? 0 or 1.\n\n• {n} to exactly n (where n is a number)\n\n• {n,m} at least n, no more than m.\n\n• {n,} n or more\n\nCharacter classes, some of the more important:\n\n• . any character except newline\n\n• \\d a decimal digit and \\D, not a decimal digit\n\n• \\s whitespace and \\S, not whitespace\n\n• \\w a \"word\" character and \\W, a \"non-word\" character\n\n• [...] (where \"...\" is some characters) is a character set\n\n• [^...] is the inverse of the given character set\n\n• [x-y] is the range of characters from x-y\n\n• [[:xxx:]] is a named character set\n\n• [[:^xxx:]] is the inverse of a named character set\n\n• [[:alnum:]] : \"alphanumeric\"\n\n• [[:alpha:]] : \"alphabetic\"\n\n• [[:ascii:]] : \"0-127\"\n\n• [[:blank:]] : \"space or tab\"\n\n• [[:cntrl:]] : \"control character\"\n\n• [[:digit:]] : \"decimal digit\"\n\n• [[:graph:]] : \"printing, excluding space\"\n\n• [[:lower:]] : \"lower case letter\"\n\n• [[:print:]] : \"printing, including space\"\n\n• [[:punct:]] : \"printing, excluding alphanumeric\"\n\n• [[:space:]] : \"white space\"\n\n• [[:upper:]] : \"upper case letter\"\n\n• [[:word:]] : \"same as w\"\n\n• [[:xdigit:]] : \"hexadecimal digit\"\n\nGroups:\n\n• (...) is a capturing group\n\n• (?:...) is a non-capturing group\n\n• \\n is a backreference (where n is the number of the group, starting\n with 1)\n\n• $n is a reference from the replacement expression to a group in the\n match expression.\n\nAnd some other things:\n\n• \\b denotes a word boundary, \\B is not a word boundary.\n\n• ^ is the start of the string or line, $ the end.\n\n• | is \"alternation\", i.e. the \"or\".\n\nCOMPARISON TO OTHER TOOLS\nMost operations string supports can also be done by external tools.\nSome of these include grep, sed and cut.\n\nIf you are familiar with these, it is useful to know how string\ndiffers from them.\n\nIn contrast to these classics, string reads input either from stdin\nor as arguments. string also does not deal with files, so it requires\nredirections to be used with them.\n\nIn contrast to grep, string's match defaults to glob-mode, while\nreplace defaults to literal matching. If set to regex-mode, they use\nPCRE regular expressions, which is comparable to grep's -P option.\nmatch defaults to printing just the match, which is like grep with -o\n(use --entire to enable grep-like behavior).\n\nLike sed's s/old/new/ command, string replace still prints strings\nthat don't match. sed's -n in combination with a /p modifier or\ncommand is like string replace -f.\n\nstring split somedelimiter is a replacement for tr somedelimiter \\n.", + "args": "string collect [-a | --allow-empty] [-N | --no-trim-newlines] [STRING ...]\nstring escape [-n | --no-quoted] [--style=] [STRING ...]\nstring join [-q | --quiet] [-n | --no-empty] SEP [STRING ...]\nstring join0 [-q | --quiet] [STRING ...]\nstring length [-q | --quiet] [STRING ...]\nstring lower [-q | --quiet] [STRING ...]\nstring match [-a | --all] [-e | --entire] [-i | --ignore-case]\n [-g | --groups-only] [-r | --regex] [-n | --index]\n [-q | --quiet] [-v | --invert]\n PATTERN [STRING ...]\nstring pad [-r | --right] [(-c | --char) CHAR] [(-w | --width) INTEGER]\n [STRING ...]\nstring repeat [(-n | --count) COUNT] [(-m | --max) MAX] [-N | --no-newline]\n [-q | --quiet] [STRING ...]\nstring replace [-a | --all] [-f | --filter] [-i | --ignore-case]\n [-r | --regex] [-q | --quiet] PATTERN REPLACE [STRING ...]\nstring shorten [(-c | --char) CHARS] [(-m | --max) INTEGER]\n [-N | --no-newline] [-l | --left] [-q | --quiet] [STRING ...]\nstring split [(-f | --fields) FIELDS] [(-m | --max) MAX] [-n | --no-empty]\n [-q | --quiet] [-r | --right] SEP [STRING ...]\nstring split0 [(-f | --fields) FIELDS] [(-m | --max) MAX] [-n | --no-empty]\n [-q | --quiet] [-r | --right] [STRING ...]\nstring sub [(-s | --start) START] [(-e | --end) END] [(-l | --length) LENGTH]\n [-q | --quiet] [STRING ...]\nstring trim [-l | --left] [-r | --right] [(-c | --chars) CHARS]\n [-q | --quiet] [STRING ...]\nstring unescape [--style=] [STRING ...]\nstring upper [-q | --quiet] [STRING ...]" + }, + "switch": { + "shortDescription": "conditionally execute a block of commands", + "description": "switch performs one of several blocks of commands, depending on\nwhether a specified value equals one of several globbed values. case\nis used together with the switch statement in order to determine\nwhich block should be executed.\n\nEach case command is given one or more parameters. The first case\ncommand with a parameter that matches the string specified in the\nswitch command will be evaluated. case parameters may contain globs.\nThese need to be escaped or quoted in order to avoid regular glob\nexpansion using filenames.\n\nNote that fish does not fall through on case statements. Only the\nfirst matching case is executed.\n\nNote that break cannot be used to exit a case/switch block early like\nin other languages. It can only be used in loops.\n\nNote that command substitutions in a case statement will be evaluated\neven if its body is not taken. All substitutions, including command\nsubstitutions, must be performed before the value can be compared\nagainst the parameter.\n\nEXAMPLE\nIf the variable $animal contains the name of an animal, the following\ncode would attempt to classify it:\n\n switch $animal\n case cat\n echo evil\n case wolf dog human moose dolphin whale\n echo mammal\n case duck goose albatross\n echo bird\n case shark trout stingray\n echo fish\n case '*'\n echo I have no idea what a $animal is\n end\n\nIf the above code was run with $animal set to whale, the output would\nbe mammal.", + "args": "switch VALUE; [case [GLOB ...]; [COMMANDS ...]; ...] end" + }, + "test": { + "shortDescription": "Evaluate conditional expressions", + "description": "The `test` command evaluates conditional expressions and sets the exit status to 0 if the expression is true, and 1 if it is false. It supports various operators to evaluate expressions related to strings, numbers, and file attributes.", + "args": "EXPRESSION" + }, + "time": { + "shortDescription": "measure how long a command or block takes", + "description": "NOTE: This page documents the fish keyword time. To see the\ndocumentation on the time command you might have, use command man\ntime.\n\ntime causes fish to measure how long a command takes and print the\nresults afterwards. The command can be a simple fish command or a\nblock. The results can not currently be redirected.\n\nFor checking timing after a command has completed, check\n$CMD_DURATION.\n\nYour system most likely also has a time command. To use that use\nsomething like command time, as in command time sleep 10. Because\nit's not inside fish, it won't have access to fish functions and\nwon't be able to time blocks and such.\n\nHOW TO INTERPRET THE OUTPUT\nTime outputs a few different values. Let's look at an example:\n\n > time string repeat -n 10000000 y\\n | command grep y >/dev/null\n _______________________________________________________\n Executed in 805.98 millis fish external\n usr time 798.88 millis 763.88 millis 34.99 millis\n sys time 141.22 millis 40.20 millis 101.02 millis\n\nThe time after \"Executed in\" is what is known as the \"wall-clock\ntime\". It is simply a measure of how long it took from the start of\nthe command until it finished. Typically it is reasonably close to\nCMD_DURATION, except for a slight skew because the two are taken at\nslightly different times.\n\nThe other times are all measures of CPU time. That means they measure\nhow long the CPU was used in this part, and they count multiple cores\nseparately. So a program with four threads using all CPU for a second\nwill have a time of 4 seconds.\n\nThe \"usr\" time is how much CPU time was spent inside the program\nitself, the \"sys\" time is how long was spent in the kernel on behalf\nof that program.\n\nThe \"fish\" time is how much CPU was spent in fish, the \"external\"\ntime how much was spent in external commands.\n\nSo in this example, since string is a builtin, everything that string\nrepeat did is accounted to fish. Any time it spends doing syscalls\nlike write() is accounted for in the fish/sys time.\n\nAnd grep here is explicitly invoked as an external command, so its\ntimes will be counted in the \"external\" column.\n\nNote that, as in this example, the CPU times can add up to more than\nthe execution time. This is because things can be done in parallel -\ngrep can match while string repeat writes.\n\nEXAMPLE\n(for obvious reasons exact results will vary on your system)\n\n > time sleep 1s\n\n _______________________________________________________\n Executed in 1,01 secs fish external\n usr time 2,32 millis 0,00 micros 2,32 millis\n sys time 0,88 millis 877,00 micros 0,00 millis\n\n > time for i in 1 2 3; sleep 1s; end\n\n _______________________________________________________\n Executed in 3,01 secs fish external\n usr time 9,16 millis 2,94 millis 6,23 millis\n sys time 0,23 millis 0,00 millis 0,23 millis\n\nInline variable assignments need to follow the time keyword:\n\n > time a_moment=1.5m sleep $a_moment\n\n _______________________________________________________\n Executed in 90.00 secs fish external\n usr time 4.62 millis 4.62 millis 0.00 millis\n sys time 2.35 millis 0.41 millis 1.95 millis", + "args": "time COMMAND" + }, + "true": { + "shortDescription": "Return a successful result", + "description": "The `true` command always returns a successful (zero) exit status. It is often used in scripts and conditional statements where an unconditional success result is needed." + }, + "type": { + "shortDescription": "locate a command and describe its type", + "description": "With no options, type indicates how each NAME would be interpreted if\nused as a command name.\n\nThe following options are available:\n\n-a or --all\n Prints all of possible definitions of the specified names.\n\n-s or --short\n Suppresses function expansion when used with no options or\n with -a/--all.\n\n-f or --no-functions\n Suppresses function and builtin lookup.\n\n-t or --type\n Prints function, builtin, or file if NAME is a shell function,\n builtin, or disk file, respectively.\n\n-p or --path\n Prints the path to NAME if NAME resolves to an executable file\n in PATH, the path to the script containing the definition of\n the function NAME if NAME resolves to a function loaded from a\n file on disk (i.e. not interactively defined at the prompt),\n or nothing otherwise.\n\n-P or --force-path\n Returns the path to the executable file NAME, presuming NAME\n is found in the PATH environment variable, or nothing\n otherwise. --force-path explicitly resolves only the path to\n executable files in PATH, regardless of whether NAME is\n shadowed by a function or builtin with the same name.\n\n-q or --query\n Suppresses all output; this is useful when testing the exit\n status. For compatibility with old fish versions this is also\n --quiet.\n\n-h or --help\n Displays help about using this command.\n\nThe -q, -p, -t and -P flags (and their long flag aliases) are\nmutually exclusive. Only one can be specified at a time.\n\ntype returns 0 if at least one entry was found, 1 otherwise, and 2\nfor invalid options or option combinations.\n\nEXAMPLE\n\n > type fg\n fg is a builtin", + "args": "type [OPTIONS] NAME [...]" + }, + "ulimit": { + "shortDescription": "set or get resource usage limits", + "description": "ulimit sets or outputs the resource usage limits of the shell and any\nprocesses spawned by it. If a new limit value is omitted, the current\nvalue of the limit of the resource is printed; otherwise, the\nspecified limit is set to the new value.\n\nUse one of the following switches to specify which resource limit to\nset or report:\n\n-b or --socket-buffers\n The maximum size of socket buffers.\n\n-c or --core-size\n The maximum size of core files created. By setting this limit\n to zero, core dumps can be disabled.\n\n-d or --data-size\n The maximum size of a process' data segment.\n\n-e or --nice\n Controls the maximum nice value; on Linux, this value is\n subtracted from 20 to give the effective value.\n\n-f or --file-size\n The maximum size of files created by a process.\n\n-i or --pending-signals\n The maximum number of signals that may be queued.\n\n-l or --lock-size\n The maximum size that may be locked into memory.\n\n-m or --resident-set-size\n The maximum resident set size.\n\n-n or --file-descriptor-count\n The maximum number of open file descriptors.\n\n-q or --queue-size\n The maximum size of data in POSIX message queues.\n\n-r or --realtime-priority\n The maximum realtime scheduling priority.\n\n-s or --stack-size\n The maximum stack size.\n\n-t or --cpu-time\n The maximum amount of CPU time in seconds.\n\n-u or --process-count\n The maximum number of processes available to the current user.\n\n-w or --swap-size\n The maximum swap space available to the current user.\n\n-v or --virtual-memory-size\n The maximum amount of virtual memory available to the shell.\n\n-y or --realtime-maxtime\n The maximum contiguous realtime CPU time in microseconds.\n\n-K or --kernel-queues\n The maximum number of kqueues (kernel queues) for the current\n user.\n\n-P or --ptys\n The maximum number of pseudo-terminals for the current user.\n\n-T or --threads\n The maximum number of simultaneous threads for the current\n user.\n\nNote that not all these limits are available in all operating\nsystems; consult the documentation for setrlimit in your operating\nsystem.\n\nThe value of limit can be a number in the unit specified for the\nresource or one of the special values hard, soft, or unlimited, which\nstand for the current hard limit, the current soft limit, and no\nlimit, respectively.\n\nIf limit is given, it is the new value of the specified resource. If\nno option is given, then -f is assumed. Values are in kilobytes,\nexcept for -t, which is in seconds and -n and -u, which are unscaled\nvalues. The exit status is 0 unless an invalid option or argument is\nsupplied, or an error occurs while setting a new limit.\n\nulimit also accepts the following options that determine what type of\nlimit to set:\n\n-H or --hard\n Sets hard resource limit.\n\n-S or --soft\n Sets soft resource limit.\n\nA hard limit can only be decreased. Once it is set it cannot be\nincreased; a soft limit may be increased up to the value of the hard\nlimit. If neither -H nor -S is specified, both the soft and hard\nlimits are updated when assigning a new limit value, and the soft\nlimit is used when reporting the current value.\n\nThe following additional options are also understood by ulimit:\n\n-a or --all\n Prints all current limits.\n\n-h or --help\n Displays help about using this command.\n\nThe fish implementation of ulimit should behave identically to the\nimplementation in bash, except for these differences:\n\n• Fish ulimit supports GNU-style long options for all switches.\n\n• Fish ulimit does not support the -p option for getting the pipe\n size. The bash implementation consists of a compile-time check that\n empirically guesses this number by writing to a pipe and waiting\n for SIGPIPE. Fish does not do this because this method of\n determining pipe size is unreliable. Depending on bash version,\n there may also be further additional limits to set in bash that do\n not exist in fish.\n\n• Fish ulimit does not support getting or setting multiple limits in\n one command, except reporting all values using the -a switch.\n\nEXAMPLE\nulimit -Hs 64 sets the hard stack size limit to 64 kB.", + "args": "ulimit [OPTIONS] [LIMIT]" + }, + "wait": { + "shortDescription": "wait for jobs to complete", + "description": "wait waits for child jobs to complete.\n\nIf a PID is specified, the command waits for the job that the process\nwith that process ID belongs to.\n\nIf a PROCESS_NAME is specified, the command waits for the jobs that\nthe matched processes belong to.\n\nIf neither a pid nor a process name is specified, the command waits\nfor all background jobs.\n\nIf the -n or --any flag is provided, the command returns as soon as\nthe first job completes. If it is not provided, it returns after all\njobs complete.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\n\n sleep 10 &\n wait $last_pid\n\nspawns sleep in the background, and then waits until it finishes.\n\n for i in (seq 1 5); sleep 10 &; end\n wait\n\nspawns five jobs in the background, and then waits until all of them\nfinishes.\n\n for i in (seq 1 5); sleep 10 &; end\n hoge &\n wait sleep\n\nspawns five jobs and hoge in the background, and then waits until all\nsleeps finish, and doesn't wait for hoge finishing.", + "args": "wait [-n | --any] [PID | PROCESS_NAME] ..." + }, + "while": { + "shortDescription": "perform a set of commands multiple times", + "description": "while repeatedly executes CONDITION, and if the exit status is 0,\nthen executes COMMANDS.\n\nThe exit status of the while loop is the exit status of the last\niteration of the COMMANDS executed, or 0 if none were executed. (This\nmatches other shells and is POSIX-compatible.)\n\nYou can use and or or for complex conditions. Even more complex\ncontrol can be achieved with while true containing a break.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\n\n while test -f foo.txt; or test -f bar.txt ; echo file exists; sleep 10; end\n # outputs 'file exists' at 10 second intervals,\n # as long as the file foo.txt or bar.txt exists.", + "args": "while CONDITION; COMMANDS; end" + } +} as const; \ No newline at end of file diff --git a/extensions/terminal-suggest/src/shell/zsh.ts b/extensions/terminal-suggest/src/shell/zsh.ts index 7718b4a1c16..64ba4ed3d42 100644 --- a/extensions/terminal-suggest/src/shell/zsh.ts +++ b/extensions/terminal-suggest/src/shell/zsh.ts @@ -7,10 +7,9 @@ import * as vscode from 'vscode'; import type { ICompletionResource } from '../types'; import { execHelper, getAliasesHelper } from './common'; import { type ExecOptionsWithStringEncoding } from 'node:child_process'; -import * as path from 'path'; -import * as fs from 'fs'; +import { zshBuiltinsCommandDescriptionsCache } from './zshBuiltinsCache'; -let zshBuiltinsCommandDescriptionsCache: Map | undefined; +const commandDescriptionsCache: Map | undefined = parseCache(zshBuiltinsCommandDescriptionsCache); export async function getZshGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { return [ @@ -20,7 +19,8 @@ export async function getZshGlobals(options: ExecOptionsWithStringEncoding, exis } async function getAliases(options: ExecOptionsWithStringEncoding): Promise { - return getAliasesHelper('zsh', ['-ic', 'alias'], /^(?[a-zA-Z0-9\._:-]+)=(?['"]?)(?.+?)\k$/, options); + const args = process.platform === 'darwin' ? ['-icl', 'alias'] : ['-ic', 'alias']; + return getAliasesHelper('zsh', args, /^(?[a-zA-Z0-9\._:-]+)=(?['"]?)(?.+?)\k$/, options); } async function getBuiltins( @@ -39,22 +39,8 @@ async function getBuiltins( }); } - if (!zshBuiltinsCommandDescriptionsCache) { - const cacheFilePath = path.join(__dirname, 'zshBuiltinsCache.json'); - if (fs.existsSync(cacheFilePath)) { - try { - const cacheFileContent = fs.readFileSync(cacheFilePath, 'utf8'); - const cacheObject = JSON.parse(cacheFileContent); - zshBuiltinsCommandDescriptionsCache = new Map(Object.entries(cacheObject)); - } catch (e) { - console.error('Failed to load zsh builtins cache', e); - } - } else { - console.warn('zsh builtins cache not found'); - } - } - for (const cmd of zshBuiltinsCommandDescriptionsCache?.keys() ?? []) { + for (const cmd of commandDescriptionsCache?.keys() ?? []) { if (typeof cmd === 'string') { try { const result = getCommandDescription(cmd); @@ -83,7 +69,7 @@ export function getCommandDescription(command: string): { documentation?: string if (!zshBuiltinsCommandDescriptionsCache) { return undefined; } - const result = zshBuiltinsCommandDescriptionsCache?.get(command); + const result = commandDescriptionsCache?.get(command); if (result?.shortDescription) { return { description: result.shortDescription, @@ -98,3 +84,14 @@ export function getCommandDescription(command: string): { documentation?: string }; } } + +function parseCache(cache: Object): Map | undefined { + if (!cache) { + return undefined; + } + const result = new Map(); + for (const [key, value] of Object.entries(cache)) { + result.set(key, value); + } + return result; +} diff --git a/extensions/terminal-suggest/src/shell/zshBuiltinsCache.json b/extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts similarity index 99% rename from extensions/terminal-suggest/src/shell/zshBuiltinsCache.json rename to extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts index 62b17d69f0a..6626192ce98 100644 --- a/extensions/terminal-suggest/src/shell/zshBuiltinsCache.json +++ b/extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts @@ -1,4 +1,10 @@ -{ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const zshBuiltinsCommandDescriptionsCache = { ".": { "shortDescription": "Source a file", "description": ": Read commands from *file* and execute them in the current shell environment.\n\n If *file* does not contain a slash, or if **PATH_DIRS** is set, the shell looks in the components of **\\$path** to find the directory containing *file*. Files in the current directory are not read unless `**.**` appears somewhere in **\\$path**. If a file named `*file***.zwc**` is found, is newer than *file*, and is the compiled form (created with the **zcompile** builtin) of *file*, then commands are read from that file instead of *file*.\n\n If any arguments *arg* are given, they become the positional parameters; the old positional parameters are restored when the *file* is done executing. However, if no arguments are given, the positional parameters remain those of the calling context, and no restoring is done.\n\n If *file* was not found the return status is 127; if *file* was found but contained a syntax error the return status is 126; else the return status is the exit status of the last command executed.", @@ -534,4 +540,4 @@ "description": ": See the section `The zsh/net/tcp Module` in *zshmodules(1).*", "args": "ztcp" } -} \ No newline at end of file +} as const; \ No newline at end of file diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 5138d5f7f80..3abd2e8efd1 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -10,6 +10,7 @@ import * as vscode from 'vscode'; import cdSpec from './completions/cd'; import codeCompletionSpec from './completions/code'; import codeInsidersCompletionSpec from './completions/code-insiders'; +import npxCompletionSpec from './completions/npx'; import setLocationSpec from './completions/set-location'; import { upstreamSpecs } from './constants'; import { PathExecutableCache } from './env/pathExecutableCache'; @@ -24,22 +25,14 @@ import type { ICompletionResource } from './types'; import { createCompletionItem } from './helpers/completionItem'; import { getFigSuggestions } from './fig/figInterface'; import { executeCommand, executeCommandTimeout, IFigExecuteExternals } from './fig/execute'; +import { createTimeoutPromise } from './helpers/promise'; -// TODO: remove once API is finalized export const enum TerminalShellType { - Sh = 1, - Bash = 2, - Fish = 3, - Csh = 4, - Ksh = 5, - Zsh = 6, - CommandPrompt = 7, - GitBash = 8, - PowerShell = 9, - Python = 10, - Julia = 11, - NuShell = 12, - Node = 13 + Bash = 'bash', + Fish = 'fish', + Zsh = 'zsh', + PowerShell = 'pwsh', + Python = 'python' } const isWindows = osIsWindows(); @@ -50,6 +43,7 @@ export const availableSpecs: Fig.Spec[] = [ cdSpec, codeInsidersCompletionSpec, codeCompletionSpec, + npxCompletionSpec, setLocationSpec, ]; for (const spec of upstreamSpecs) { @@ -70,11 +64,10 @@ async function getShellGlobals(shellType: TerminalShellType, existingCommands?: if (cachedCommands) { return cachedCommands; } - const shell = getShell(shellType); - if (!shell) { + if (!shellType) { return; } - const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; + const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell: shellType }; const mixedCommands: (string | ICompletionResource)[] | undefined = await getShellSpecificGlobals.get(shellType)?.(options, existingCommands); const normalizedCommands = mixedCommands?.map(command => typeof command === 'string' ? ({ label: command }) : command); cachedGlobals.set(shellType, normalizedCommands); @@ -94,25 +87,46 @@ export async function activate(context: vscode.ExtensionContext) { id: 'terminal-suggest', async provideTerminalCompletions(terminal: vscode.Terminal, terminalContext: vscode.TerminalCompletionContext, token: vscode.CancellationToken): Promise { if (token.isCancellationRequested) { + console.debug('#terminalCompletions token cancellation requested'); return; } - const shellType: TerminalShellType | undefined = 'shellType' in terminal.state ? terminal.state.shellType as TerminalShellType : undefined; - if (!shellType) { + const shellType: string | undefined = 'shell' in terminal.state ? terminal.state.shell as string : undefined; + const terminalShellType = getTerminalShellType(shellType); + if (!terminalShellType) { + console.debug('#terminalCompletions No shell type found for terminal'); return; } const commandsInPath = await pathExecutableCache.getExecutablesInPath(terminal.shellIntegration?.env?.value); - const shellGlobals = await getShellGlobals(shellType, commandsInPath?.labels) ?? []; + const shellGlobals = await getShellGlobals(terminalShellType, commandsInPath?.labels) ?? []; if (!commandsInPath?.completionResources) { + console.debug('#terminalCompletions No commands found in path'); return; } // Order is important here, add shell globals first so they are prioritized over path commands const commands = [...shellGlobals, ...commandsInPath.completionResources]; const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); const pathSeparator = isWindows ? '\\' : '/'; - const tokenType = getTokenType(terminalContext, shellType); - const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, tokenType, terminal.shellIntegration?.cwd, getEnvAsRecord(terminal.shellIntegration?.env?.value), terminal.name, token); + const tokenType = getTokenType(terminalContext, terminalShellType); + const result = await Promise.race([ + getCompletionItemsFromSpecs( + availableSpecs, + terminalContext, + commands, + prefix, + tokenType, + terminal.shellIntegration?.cwd, + getEnvAsRecord(terminal.shellIntegration?.env?.value), + terminal.name, + token + ), + createTimeoutPromise(300, undefined) + ]); + if (!result) { + return; + } + if (terminal.shellIntegration?.env) { const homeDirCompletion = result.items.find(i => i.label === '~'); if (homeDirCompletion && terminal.shellIntegration.env?.value?.HOME) { @@ -169,8 +183,8 @@ export async function resolveCwdFromPrefix(prefix: string, currentCwd?: vscode.U // Ignore errors } - // If the prefix is not a folder, return the current cwd - return currentCwd; + // No valid path found + return undefined; } function getPrefix(commandLine: string, cursorPosition: number): string { @@ -277,7 +291,7 @@ export async function getCompletionItemsFromSpecs( let cwd: vscode.Uri | undefined; if (shellIntegrationCwd && (filesRequested || foldersRequested)) { - cwd = await resolveCwdFromPrefix(prefix, shellIntegrationCwd) ?? shellIntegrationCwd; + cwd = await resolveCwdFromPrefix(prefix, shellIntegrationCwd); } return { items, filesRequested, foldersRequested, fileExtensions, cwd }; @@ -297,30 +311,56 @@ function compareItems(existingItem: vscode.TerminalCompletionItem, command: ICom } } -function getShell(shellType: TerminalShellType): string | undefined { - switch (shellType) { - case TerminalShellType.Bash: - return 'bash'; - case TerminalShellType.Fish: - return 'fish'; - case TerminalShellType.Zsh: - return 'zsh'; - case TerminalShellType.PowerShell: - return 'pwsh'; - default: { - return undefined; - } - } -} - function getEnvAsRecord(shellIntegrationEnv: { [key: string]: string | undefined } | undefined): Record { const env: Record = {}; - if (shellIntegrationEnv) { - for (const [key, value] of Object.entries(shellIntegrationEnv)) { - if (typeof value === 'string') { - env[key] = value; - } + for (const [key, value] of Object.entries(shellIntegrationEnv ?? process.env)) { + if (typeof value === 'string') { + env[key] = value; } } + if (!shellIntegrationEnv) { + sanitizeProcessEnvironment(env); + } return env; } + +function getTerminalShellType(shellType: string | undefined): TerminalShellType | undefined { + switch (shellType) { + case 'bash': + return TerminalShellType.Bash; + case 'zsh': + return TerminalShellType.Zsh; + case 'pwsh': + return TerminalShellType.PowerShell; + case 'fish': + return TerminalShellType.Fish; + case 'python': + return TerminalShellType.Python; + default: + return undefined; + } +} + +export function sanitizeProcessEnvironment(env: Record, ...preserve: string[]): void { + const set = preserve.reduce>((set, key) => { + set[key] = true; + return set; + }, {}); + const keysToRemove = [ + /^ELECTRON_.$/, + /^VSCODE_(?!(PORTABLE|SHELL_LOGIN|ENV_REPLACE|ENV_APPEND|ENV_PREPEND)).$/, + /^SNAP(|_.*)$/, + /^GDK_PIXBUF_.$/, + ]; + const envKeys = Object.keys(env); + envKeys + .filter(key => !set[key]) + .forEach(envKey => { + for (let i = 0; i < keysToRemove.length; i++) { + if (envKey.search(keysToRemove[i]) !== -1) { + delete env[envKey]; + break; + } + } + }); +} diff --git a/extensions/terminal-suggest/src/test/completions/code.test.ts b/extensions/terminal-suggest/src/test/completions/code.test.ts index e90d3120ff4..a7b6885f83d 100644 --- a/extensions/terminal-suggest/src/test/completions/code.test.ts +++ b/extensions/terminal-suggest/src/test/completions/code.test.ts @@ -8,8 +8,7 @@ import codeCompletionSpec from '../../completions/code'; import { testPaths, type ISuiteSpec, type ITestSpec } from '../helpers'; import codeInsidersCompletionSpec from '../../completions/code-insiders'; -export const codeSpecOptions = ['-', '--add', '--category', '--diff', '--disable-extension', '--disable-extensions', '--disable-gpu', '--enable-proposed-api', '--extensions-dir', '--goto', '--help', '--inspect-brk-extensions', '--inspect-extensions', '--install-extension', '--list-extensions', '--locale', '--log', '--max-memory', '--merge', '--new-window', '--pre-release', '--prof-startup', '--profile', '--reuse-window', '--show-versions', '--status', '--sync', '--telemetry', '--uninstall-extension', '--user-data-dir', '--verbose', '--version', '--wait', '-a', '-d', '-g', '-h', '-m', '-n', '-r', '-s', '-v', '-w']; - +export const codeSpecOptions = ['-', '--add', '--category', '--diff', '--disable-extension', '--disable-extensions', '--disable-gpu', '--enable-proposed-api', '--extensions-dir', '--goto', '--help', '--inspect-brk-extensions', '--inspect-extensions', '--install-extension', '--list-extensions', '--locale', '--locate-shell-integration-path', '--log', '--max-memory', '--merge', '--new-window', '--pre-release', '--prof-startup', '--profile', '--reuse-window', '--show-versions', '--status', '--sync', '--telemetry', '--uninstall-extension', '--user-data-dir', '--verbose', '--version', '--wait', '-a', '-d', '-g', '-h', '-m', '-n', '-r', '-s', '-v', '-w']; export function createCodeTestSpecs(executable: string): ITestSpec[] { const localeOptions = ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW']; @@ -39,8 +38,9 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] { { input: `${executable} --goto |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, { input: `${executable} --user-data-dir |`, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, { input: `${executable} --profile |` }, - { input: `${executable} --install-extension |` }, - { input: `${executable} --uninstall-extension |` }, + { input: `${executable} --install-extension |`, expectedCompletions: [executable], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} --uninstall-extension |`, expectedCompletions: [executable] }, + { input: `${executable} --disable-extension |`, expectedCompletions: [executable] }, { input: `${executable} --log |`, expectedCompletions: logOptions }, { input: `${executable} --sync |`, expectedCompletions: syncOptions }, { input: `${executable} --extensions-dir |`, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, diff --git a/extensions/terminal-suggest/src/tokens.ts b/extensions/terminal-suggest/src/tokens.ts index a967aeb2054..0520a2315a4 100644 --- a/extensions/terminal-suggest/src/tokens.ts +++ b/extensions/terminal-suggest/src/tokens.ts @@ -5,18 +5,19 @@ import { TerminalShellType } from './terminalSuggestMain'; + export const enum TokenType { Command, Argument, } -const shellTypeResetChars: { [key: number]: string[] | undefined } = { - [TerminalShellType.Bash]: ['>', '>>', '<', '2>', '2>>', '&>', '&>>', '|', '|&', '&&', '||', '&', ';', '(', '{', '<<'], - [TerminalShellType.Zsh]: ['>', '>>', '<', '2>', '2>>', '&>', '&>>', '<>', '|', '|&', '&&', '||', '&', ';', '(', '{', '<<', '<<<', '<('], - [TerminalShellType.PowerShell]: ['>', '>>', '<', '2>', '2>>', '*>', '*>>', '|', '-and', '-or', '-not', '!', '&', '-eq', '-ne', '-gt', '-lt', '-ge', '-le', '-like', '-notlike', '-match', '-notmatch', '-contains', '-notcontains', '-in', '-notin'] -}; +const shellTypeResetChars = new Map([ + [TerminalShellType.Bash, ['>', '>>', '<', '2>', '2>>', '&>', '&>>', '|', '|&', '&&', '||', '&', ';', '(', '{', '<<']], + [TerminalShellType.Zsh, ['>', '>>', '<', '2>', '2>>', '&>', '&>>', '<>', '|', '|&', '&&', '||', '&', ';', '(', '{', '<<', '<<<', '<(']], + [TerminalShellType.PowerShell, ['>', '>>', '<', '2>', '2>>', '*>', '*>>', '|', '-and', '-or', '-not', '!', '&', '-eq', '-ne', '-gt', '-lt', '-ge', '-le', '-like', '-notlike', '-match', '-notmatch', '-contains', '-notcontains', '-in', '-notin']] +]); -const defaultShellTypeResetChars = shellTypeResetChars[TerminalShellType.Bash]!; +const defaultShellTypeResetChars = shellTypeResetChars.get(TerminalShellType.Bash)!; export function getTokenType(ctx: { commandLine: string; cursorPosition: number }, shellType: TerminalShellType | undefined): TokenType { const spaceIndex = ctx.commandLine.substring(0, ctx.cursorPosition).lastIndexOf(' '); @@ -24,7 +25,7 @@ export function getTokenType(ctx: { commandLine: string; cursorPosition: number return TokenType.Command; } const previousTokens = ctx.commandLine.substring(0, spaceIndex + 1).trim(); - const commandResetChars = shellType === undefined ? defaultShellTypeResetChars : shellTypeResetChars[shellType] ?? defaultShellTypeResetChars; + const commandResetChars = shellType === undefined ? defaultShellTypeResetChars : shellTypeResetChars.get(shellType) ?? defaultShellTypeResetChars; if (commandResetChars.some(e => previousTokens.endsWith(e))) { return TokenType.Command; } diff --git a/extensions/typescript-basics/language-configuration.json b/extensions/typescript-basics/language-configuration.json index e7fadc46b72..844c8189d9f 100644 --- a/extensions/typescript-basics/language-configuration.json +++ b/extensions/typescript-basics/language-configuration.json @@ -249,7 +249,7 @@ // Add // when pressing enter from inside line comment { "beforeText": { - "pattern": "(? { - const proj = await this.packageManager.resolveProject(root, await this.getInstallOpts(incomingUri.original, root)); + let proj: ResolvedProject; + try { + proj = await this.packageManager.resolveProject(root, await this.getInstallOpts(incomingUri.original, root)); + } catch (e) { + console.error(`failed to resolve project at ${incomingUri.path}: `, e); + return; + } + try { await proj.restore(); } catch (e) { console.error(`failed to restore package at ${incomingUri.path}: `, e); - throw e; } })(); this._projectCache.set(root, installing); diff --git a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts index a0d59256f10..98b14148d95 100644 --- a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts +++ b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts @@ -15,6 +15,20 @@ import FileConfigurationManager from './fileConfigurationManager'; import { conditionalRegistration, requireGlobalConfiguration, requireMinVersion, requireSomeCapability } from './util/dependentRegistration'; class CopyMetadata { + + static parse(data: string): CopyMetadata | undefined { + try { + + const parsedData = JSON.parse(data); + const resource = vscode.Uri.parse(parsedData.resource); + const ranges = parsedData.ranges.map((range: any) => new vscode.Range(range.start, range.end)); + const copyOperation = parsedData.copyOperation ? Promise.resolve(parsedData.copyOperation) : undefined; + return new CopyMetadata(resource, ranges, copyOperation); + } catch (error) { + return undefined; + } + } + constructor( public readonly resource: vscode.Uri, public readonly ranges: readonly vscode.Range[], @@ -213,7 +227,15 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { + token: vscode.CancellationToken + ): Promise { const filepath = this.client.toOpenTsFilePath(document); if (!filepath) { return undefined; } - const enableExpandableHover = vscode.workspace.getConfiguration('typescript').get('experimental.expandableHover'); - let verbosityLevel: number | undefined; - if (enableExpandableHover && this.client.apiVersion.gte(API.v570)) { - verbosityLevel = Math.max(0, this.getPreviousLevel(context?.previousHover) + (context?.verbosityDelta ?? 0)); - } - const args = { ...typeConverters.Position.toFileLocationRequestArgs(filepath, position), verbosityLevel }; - const response = await this.client.interruptGetErr(async () => { await this.fileConfigurationManager.ensureConfigurationForDocument(document, token); + const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position); return this.client.execute('quickinfo', args, token); }); @@ -50,24 +42,9 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { return undefined; } - const contents = this.getContents(document.uri, response.body, response._serverType); - const range = typeConverters.Range.fromTextSpan(response.body); - const hover = verbosityLevel !== undefined ? - new vscode.VerboseHover( - contents, - range, - // @ts-expect-error - /*canIncreaseVerbosity*/ response.body.canIncreaseVerbosityLevel, - /*canDecreaseVerbosity*/ verbosityLevel !== 0 - ) : new vscode.Hover( - contents, - range - ); - - if (verbosityLevel !== undefined) { - this.lastHoverAndLevel = [hover, verbosityLevel]; - } - return hover; + return new vscode.Hover( + this.getContents(document.uri, response.body, response._serverType), + typeConverters.Range.fromTextSpan(response.body)); } private getContents( @@ -95,13 +72,6 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { parts.push(md); return parts; } - - private getPreviousLevel(previousHover: vscode.Hover | undefined): number { - if (previousHover && this.lastHoverAndLevel && this.lastHoverAndLevel[0] === previousHover) { - return this.lastHoverAndLevel[1]; - } - return 0; - } } export function register( diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index 56d2896fa33..7b47be8d32f 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -191,7 +191,20 @@ class SyncedBuffer { } private getProjectRootPath(resource: vscode.Uri): string | undefined { - const workspaceRoot = this.client.getWorkspaceRootForResource(resource); + let workspaceRoot = this.client.getWorkspaceRootForResource(resource); + + // If we didn't find a real workspace, we still want to try sending along a workspace folder + // to prevent TS from loading projects from outside of any workspace. + // Just pick the highest level one on the same FS even though the file is outside of it + if (!workspaceRoot && vscode.workspace.workspaceFolders) { + for (const root of Array.from(vscode.workspace.workspaceFolders).sort((a, b) => a.uri.path.length - b.uri.path.length)) { + if (root.uri.scheme === resource.scheme && root.uri.authority === resource.authority) { + workspaceRoot = root.uri; + break; + } + } + } + if (workspaceRoot) { const tsRoot = this.client.toTsFilePath(workspaceRoot); return tsRoot?.startsWith(inMemoryResourcePrefix) ? undefined : tsRoot; diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index d9f47c1b85a..4201d6da29b 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -836,9 +836,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType } } - for (const root of roots.sort((a, b) => a.uri.fsPath.length - b.uri.fsPath.length)) { + // Find the highest level workspace folder that contains the file + for (const root of roots.sort((a, b) => a.uri.path.length - b.uri.path.length)) { if (root.uri.scheme === resource.scheme && root.uri.authority === resource.authority) { - if (resource.fsPath.startsWith(root.uri.fsPath + path.sep)) { + if (resource.path.startsWith(root.uri.path + '/')) { return root.uri; } } diff --git a/extensions/typescript-language-features/src/ui/intellisenseStatus.ts b/extensions/typescript-language-features/src/ui/intellisenseStatus.ts index e26e2b3719f..2c3ffb783c8 100644 --- a/extensions/typescript-language-features/src/ui/intellisenseStatus.ts +++ b/extensions/typescript-language-features/src/ui/intellisenseStatus.ts @@ -182,8 +182,8 @@ export class IntellisenseStatus extends Disposable { statusItem.command = { command: this.createOrOpenConfigCommandId, title: this._state.projectType === ProjectType.TypeScript - ? vscode.l10n.t("Configure tsconfig") - : vscode.l10n.t("Configure jsconfig"), + ? vscode.l10n.t("Configure TSConfig") + : vscode.l10n.t("Configure JSConfig"), arguments: [rootPath, this._state.projectType] satisfies CreateOrOpenConfigCommandArgs, }; } else { @@ -191,7 +191,7 @@ export class IntellisenseStatus extends Disposable { statusItem.detail = undefined; statusItem.command = { command: this.openOpenConfigCommandId, - title: vscode.l10n.t("Open config file"), + title: vscode.l10n.t("Open Config File"), arguments: [rootPath, this._state.projectType] satisfies CreateOrOpenConfigCommandArgs, }; } @@ -200,8 +200,8 @@ export class IntellisenseStatus extends Disposable { case IntellisenseState.Type.SyntaxOnly: { const statusItem = this.ensureStatusItem(); statusItem.severity = vscode.LanguageStatusSeverity.Warning; - statusItem.text = vscode.l10n.t("Partial Mode"); - statusItem.detail = vscode.l10n.t("Project Wide IntelliSense not available"); + statusItem.text = vscode.l10n.t("Partial mode"); + statusItem.detail = vscode.l10n.t("Project wide IntelliSense not available"); statusItem.busy = false; statusItem.command = { title: vscode.l10n.t("Learn More"), diff --git a/extensions/typescript-language-features/src/ui/versionStatus.ts b/extensions/typescript-language-features/src/ui/versionStatus.ts index a4f377782a8..901e548f4a8 100644 --- a/extensions/typescript-language-features/src/ui/versionStatus.ts +++ b/extensions/typescript-language-features/src/ui/versionStatus.ts @@ -23,7 +23,7 @@ export class VersionStatus extends Disposable { this._statusItem = this._register(vscode.languages.createLanguageStatusItem('typescript.version', jsTsLanguageModes)); this._statusItem.name = vscode.l10n.t("TypeScript Version"); - this._statusItem.detail = vscode.l10n.t("TypeScript Version"); + this._statusItem.detail = vscode.l10n.t("TypeScript version"); this._register(this._client.onTsServerStarted(({ version }) => this.onDidChangeTypeScriptVersion(version))); } diff --git a/extensions/typescript-language-features/tsconfig.json b/extensions/typescript-language-features/tsconfig.json index 776a71efaf8..f604952abd2 100644 --- a/extensions/typescript-language-features/tsconfig.json +++ b/extensions/typescript-language-features/tsconfig.json @@ -14,7 +14,6 @@ "../../src/vscode-dts/vscode.proposed.codeActionAI.d.ts", "../../src/vscode-dts/vscode.proposed.codeActionRanges.d.ts", "../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts", - "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts", - "../../src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts", + "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts" ] } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 9c1c097aab2..9e666879933 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -257,12 +257,12 @@ import { assertNoRpc, poll } from '../utils'; test('onDidChangeTerminalState should fire with shellType when created', async () => { const terminal = window.createTerminal(); - if (terminal.state.shellType) { + if (terminal.state.shell) { return; } await new Promise(r => { disposables.push(window.onDidChangeTerminalState(e => { - if (e === terminal && e.state.shellType) { + if (e === terminal && e.state.shell) { r(); } })); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index b75235f4473..ceb3aea00d5 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -1498,4 +1498,21 @@ suite('vscode API - workspace', () => { const updatedText = doc.getText(); assert.strictEqual(updatedText, text); }); + + test('encoding: utf8bom does not explode (https://github.com/microsoft/vscode/issues/242132)', async function () { + const buffer = [0xEF, 0xBB, 0xBF, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]; + const uri = await createRandomFile(new Uint8Array(buffer) /* UTF-8 with BOM */); + + let doc = await vscode.workspace.openTextDocument(uri); + assert.strictEqual(doc.encoding, 'utf8bom'); + + doc = await vscode.workspace.openTextDocument(uri, { encoding: 'utf8bom' }); + assert.strictEqual(doc.encoding, 'utf8bom'); + + const decoded = await vscode.workspace.decode(new Uint8Array(buffer), uri, { encoding: 'utf8bom' }); + assert.strictEqual(decoded, 'Hello World'); + + const encoded = await vscode.workspace.encode('Hello World', uri, { encoding: 'utf8bom' }); + assert.ok(equalsUint8Array(encoded, new Uint8Array(buffer))); + }); }); diff --git a/extensions/vscode-colorize-tests/test/colorize-fixtures/test-issue241715.ts b/extensions/vscode-colorize-tests/test/colorize-fixtures/test-issue241715.ts new file mode 100644 index 00000000000..ff95456b856 --- /dev/null +++ b/extensions/vscode-colorize-tests/test/colorize-fixtures/test-issue241715.ts @@ -0,0 +1,47 @@ +type obj = { [key: string]: any }; + +function destruct({ start, end, message }: any): any { return start + end + message} +function destructArr([first, second]: any): any { return first + second} + +interface WebviewMessageUpdateEverything extends WebviewMessageBase {} + +type NegNum = -1 | -2 | -10; +const n: NegNum = -10; + +export interface OptionalMethod { + optMeth?(): any; +} + +window.addEventListener('message', event => { return event }); + +export function dayOfTheWeek(date: dayjs.Dayjs): string { + return date.format('ddd'); +} + +type N = never | any | unknown; + +type Truthy = T extends '' | 0 | false | null | undefined ? never : T; +export function guardedBoolean(value: T): value is Truthy { + return Boolean(value); +} + +type Truthy = T extends '' | 0 | false | null | undefined ? never : T; + +const enum EnumName { + one = 1, +} + +void 0; + +function* makeIterator(start = 0, end = Infinity, step = 1) { +} + +function makeDate(timestamp: number): Date; +function makeDate(m: number, d: number, y: number): Date; +function makeDate(mOrTimestamp: number, d?: number, y?: number): Date { + +} + +type StringNumberBooleans = [string, number, ...boolean[]]; +type StringBooleansNumber = [string, ...boolean[], number]; +type BooleansStringNumber = [...boolean[], string, number]; diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-issue241715_ts.json b/extensions/vscode-colorize-tests/test/colorize-results/test-issue241715_ts.json new file mode 100644 index 00000000000..96a67b213ca --- /dev/null +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-issue241715_ts.json @@ -0,0 +1,6876 @@ +[ + { + "c": "type", + "t": "source.ts meta.type.declaration.ts storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "obj", + "t": "source.ts meta.type.declaration.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.type.declaration.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts meta.indexer.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "[", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts meta.indexer.declaration.ts meta.brace.square.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "key", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts meta.indexer.declaration.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts meta.indexer.declaration.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts meta.indexer.declaration.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "string", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts meta.indexer.declaration.ts meta.type.annotation.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "]", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts meta.indexer.declaration.ts meta.brace.square.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "any", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts meta.type.annotation.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.ts meta.type.declaration.ts meta.object.type.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "source.ts meta.function.ts storage.type.function.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "destruct", + "t": "source.ts meta.function.ts meta.definition.function.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.ts meta.function.ts meta.parameters.ts meta.parameter.object-binding-pattern.ts punctuation.definition.binding-pattern.object.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.parameter.object-binding-pattern.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "start", + "t": "source.ts meta.function.ts meta.parameters.ts meta.parameter.object-binding-pattern.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ",", + "t": "source.ts meta.function.ts meta.parameters.ts meta.parameter.object-binding-pattern.ts punctuation.separator.comma.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.parameter.object-binding-pattern.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "end", + "t": "source.ts meta.function.ts meta.parameters.ts meta.parameter.object-binding-pattern.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ",", + "t": "source.ts meta.function.ts meta.parameters.ts meta.parameter.object-binding-pattern.ts punctuation.separator.comma.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.parameter.object-binding-pattern.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "message", + "t": "source.ts meta.function.ts meta.parameters.ts meta.parameter.object-binding-pattern.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.parameter.object-binding-pattern.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.ts meta.function.ts meta.parameters.ts meta.parameter.object-binding-pattern.ts punctuation.definition.binding-pattern.object.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "any", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ")", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.return.type.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "any", + "t": "source.ts meta.function.ts meta.return.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "return", + "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "start", + "t": "source.ts meta.function.ts meta.block.ts variable.other.readwrite.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "+", + "t": "source.ts meta.function.ts meta.block.ts keyword.operator.arithmetic.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "end", + "t": "source.ts meta.function.ts meta.block.ts variable.other.readwrite.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "+", + "t": "source.ts meta.function.ts meta.block.ts keyword.operator.arithmetic.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "message", + "t": "source.ts meta.function.ts meta.block.ts variable.other.readwrite.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "}", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "source.ts meta.function.ts storage.type.function.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "destructArr", + "t": "source.ts meta.function.ts meta.definition.function.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "[", + "t": "source.ts meta.function.ts meta.parameters.ts meta.paramter.array-binding-pattern.ts punctuation.definition.binding-pattern.array.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "first", + "t": "source.ts meta.function.ts meta.parameters.ts meta.paramter.array-binding-pattern.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ",", + "t": "source.ts meta.function.ts meta.parameters.ts meta.paramter.array-binding-pattern.ts punctuation.separator.comma.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.paramter.array-binding-pattern.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "second", + "t": "source.ts meta.function.ts meta.parameters.ts meta.paramter.array-binding-pattern.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "]", + "t": "source.ts meta.function.ts meta.parameters.ts meta.paramter.array-binding-pattern.ts punctuation.definition.binding-pattern.array.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "any", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ")", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.return.type.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "any", + "t": "source.ts meta.function.ts meta.return.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "return", + "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "first", + "t": "source.ts meta.function.ts meta.block.ts variable.other.readwrite.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "+", + "t": "source.ts meta.function.ts meta.block.ts keyword.operator.arithmetic.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "second", + "t": "source.ts meta.function.ts meta.block.ts variable.other.readwrite.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "}", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "interface", + "t": "source.ts meta.interface.ts storage.type.interface.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.interface.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "WebviewMessageUpdateEverything", + "t": "source.ts meta.interface.ts entity.name.type.interface.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.interface.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "extends", + "t": "source.ts meta.interface.ts storage.modifier.ts", + "r": { + "dark_plus": "storage.modifier: #569CD6", + "light_plus": "storage.modifier: #0000FF", + "dark_vs": "storage.modifier: #569CD6", + "light_vs": "storage.modifier: #0000FF", + "hc_black": "storage.modifier: #569CD6", + "dark_modern": "storage.modifier: #569CD6", + "hc_light": "storage.modifier: #0F4A85", + "light_modern": "storage.modifier: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.interface.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "WebviewMessageBase", + "t": "source.ts meta.interface.ts entity.other.inherited-class.ts", + "r": { + "dark_plus": "entity.other.inherited-class: #4EC9B0", + "light_plus": "entity.other.inherited-class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.other.inherited-class: #4EC9B0", + "dark_modern": "entity.other.inherited-class: #4EC9B0", + "hc_light": "entity.other.inherited-class: #185E73", + "light_modern": "entity.other.inherited-class: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.interface.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{}", + "t": "source.ts meta.interface.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "source.ts meta.type.declaration.ts storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "NegNum", + "t": "source.ts meta.type.declaration.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.type.declaration.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " -", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "1", + "t": "source.ts meta.type.declaration.ts constant.numeric.decimal.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " -", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "2", + "t": "source.ts meta.type.declaration.ts constant.numeric.decimal.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " -", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "10", + "t": "source.ts meta.type.declaration.ts constant.numeric.decimal.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "const", + "t": "source.ts meta.var.expr.ts storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.var.expr.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "n", + "t": "source.ts meta.var.expr.ts meta.var-single-variable.expr.ts meta.definition.variable.ts variable.other.constant.ts", + "r": { + "dark_plus": "variable.other.constant: #4FC1FF", + "light_plus": "variable.other.constant: #0070C1", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable.other.constant: #4FC1FF", + "hc_light": "variable.other.constant: #02715D", + "light_modern": "variable.other.constant: #0070C1" + } + }, + { + "c": ":", + "t": "source.ts meta.var.expr.ts meta.var-single-variable.expr.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.var.expr.ts meta.var-single-variable.expr.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "NegNum", + "t": "source.ts meta.var.expr.ts meta.var-single-variable.expr.ts meta.type.annotation.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.var.expr.ts meta.var-single-variable.expr.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.var.expr.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.var.expr.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "-", + "t": "source.ts meta.var.expr.ts keyword.operator.arithmetic.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "10", + "t": "source.ts meta.var.expr.ts constant.numeric.decimal.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "export", + "t": "source.ts meta.interface.ts keyword.control.export.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": " ", + "t": "source.ts meta.interface.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "interface", + "t": "source.ts meta.interface.ts storage.type.interface.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.interface.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "OptionalMethod", + "t": "source.ts meta.interface.ts entity.name.type.interface.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.interface.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.ts meta.interface.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.interface.ts meta.method.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "optMeth", + "t": "source.ts meta.interface.ts meta.method.declaration.ts meta.definition.method.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "?", + "t": "source.ts meta.interface.ts meta.method.declaration.ts keyword.operator.optional.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "(", + "t": "source.ts meta.interface.ts meta.method.declaration.ts meta.parameters.ts punctuation.definition.parameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ")", + "t": "source.ts meta.interface.ts meta.method.declaration.ts meta.parameters.ts punctuation.definition.parameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.interface.ts meta.method.declaration.ts meta.return.type.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.interface.ts meta.method.declaration.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "any", + "t": "source.ts meta.interface.ts meta.method.declaration.ts meta.return.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ";", + "t": "source.ts meta.interface.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.ts meta.interface.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "window", + "t": "source.ts meta.function-call.ts variable.other.object.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".", + "t": "source.ts meta.function-call.ts punctuation.accessor.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "addEventListener", + "t": "source.ts meta.function-call.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "source.ts meta.brace.round.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "'", + "t": "source.ts string.quoted.single.ts punctuation.definition.string.begin.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "message", + "t": "source.ts string.quoted.single.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "source.ts string.quoted.single.ts punctuation.definition.string.end.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ",", + "t": "source.ts punctuation.separator.comma.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "event", + "t": "source.ts meta.arrow.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " ", + "t": "source.ts meta.arrow.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=>", + "t": "source.ts meta.arrow.ts storage.type.function.arrow.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.arrow.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.ts meta.arrow.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.arrow.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "return", + "t": "source.ts meta.arrow.ts meta.block.ts keyword.control.flow.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": " ", + "t": "source.ts meta.arrow.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "event", + "t": "source.ts meta.arrow.ts meta.block.ts variable.other.readwrite.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " ", + "t": "source.ts meta.arrow.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.ts meta.arrow.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ")", + "t": "source.ts meta.brace.round.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "export", + "t": "source.ts meta.function.ts keyword.control.export.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "source.ts meta.function.ts storage.type.function.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "dayOfTheWeek", + "t": "source.ts meta.function.ts meta.definition.function.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "date", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "dayjs", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts entity.name.type.module.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ".", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts punctuation.accessor.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "Dayjs", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ")", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.return.type.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "string", + "t": "source.ts meta.function.ts meta.return.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "\t", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "return", + "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "date", + "t": "source.ts meta.function.ts meta.block.ts meta.function-call.ts variable.other.object.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".", + "t": "source.ts meta.function.ts meta.block.ts meta.function-call.ts punctuation.accessor.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "format", + "t": "source.ts meta.function.ts meta.block.ts meta.function-call.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "source.ts meta.function.ts meta.block.ts meta.brace.round.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "'", + "t": "source.ts meta.function.ts meta.block.ts string.quoted.single.ts punctuation.definition.string.begin.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "ddd", + "t": "source.ts meta.function.ts meta.block.ts string.quoted.single.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "source.ts meta.function.ts meta.block.ts string.quoted.single.ts punctuation.definition.string.end.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ")", + "t": "source.ts meta.function.ts meta.block.ts meta.brace.round.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "source.ts meta.function.ts meta.block.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "source.ts meta.type.declaration.ts storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "N", + "t": "source.ts meta.type.declaration.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.type.declaration.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "never", + "t": "source.ts meta.type.declaration.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "any", + "t": "source.ts meta.type.declaration.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "unknown", + "t": "source.ts meta.type.declaration.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "source.ts meta.type.declaration.ts storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "Truthy", + "t": "source.ts meta.type.declaration.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "<", + "t": "source.ts meta.type.declaration.ts meta.type.parameters.ts punctuation.definition.typeparameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "source.ts meta.type.declaration.ts meta.type.parameters.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ">", + "t": "source.ts meta.type.declaration.ts meta.type.parameters.ts punctuation.definition.typeparameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.type.declaration.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "source.ts meta.type.declaration.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "extends", + "t": "source.ts meta.type.declaration.ts storage.modifier.ts", + "r": { + "dark_plus": "storage.modifier: #569CD6", + "light_plus": "storage.modifier: #0000FF", + "dark_vs": "storage.modifier: #569CD6", + "light_vs": "storage.modifier: #0000FF", + "hc_black": "storage.modifier: #569CD6", + "dark_modern": "storage.modifier: #569CD6", + "hc_light": "storage.modifier: #0F4A85", + "light_modern": "storage.modifier: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "'", + "t": "source.ts meta.type.declaration.ts string.quoted.single.ts punctuation.definition.string.begin.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "source.ts meta.type.declaration.ts string.quoted.single.ts punctuation.definition.string.end.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "0", + "t": "source.ts meta.type.declaration.ts constant.numeric.decimal.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "false", + "t": "source.ts meta.type.declaration.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "null", + "t": "source.ts meta.type.declaration.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "undefined", + "t": "source.ts meta.type.declaration.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "?", + "t": "source.ts meta.type.declaration.ts keyword.operator.ternary.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "never", + "t": "source.ts meta.type.declaration.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.type.declaration.ts keyword.operator.ternary.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "source.ts meta.type.declaration.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "export", + "t": "source.ts meta.function.ts keyword.control.export.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "source.ts meta.function.ts storage.type.function.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "guardedBoolean", + "t": "source.ts meta.function.ts meta.definition.function.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "<", + "t": "source.ts meta.function.ts meta.type.parameters.ts punctuation.definition.typeparameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "source.ts meta.function.ts meta.type.parameters.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ">", + "t": "source.ts meta.function.ts meta.type.parameters.ts punctuation.definition.typeparameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "(", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "value", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ")", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.return.type.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "value", + "t": "source.ts meta.function.ts meta.return.type.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "is", + "t": "source.ts meta.function.ts meta.return.type.ts keyword.operator.expression.is.ts", + "r": { + "dark_plus": "keyword.operator.expression: #569CD6", + "light_plus": "keyword.operator.expression: #0000FF", + "dark_vs": "keyword.operator.expression: #569CD6", + "light_vs": "keyword.operator.expression: #0000FF", + "hc_black": "keyword.operator.expression: #569CD6", + "dark_modern": "keyword.operator.expression: #569CD6", + "hc_light": "keyword.operator.expression: #0F4A85", + "light_modern": "keyword.operator.expression: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "Truthy", + "t": "source.ts meta.function.ts meta.return.type.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "<", + "t": "source.ts meta.function.ts meta.return.type.ts meta.type.parameters.ts punctuation.definition.typeparameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "source.ts meta.function.ts meta.return.type.ts meta.type.parameters.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ">", + "t": "source.ts meta.function.ts meta.return.type.ts meta.type.parameters.ts punctuation.definition.typeparameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "\t", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "return", + "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "Boolean", + "t": "source.ts meta.function.ts meta.block.ts meta.function-call.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "source.ts meta.function.ts meta.block.ts meta.brace.round.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "value", + "t": "source.ts meta.function.ts meta.block.ts variable.other.readwrite.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ")", + "t": "source.ts meta.function.ts meta.block.ts meta.brace.round.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "source.ts meta.function.ts meta.block.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "source.ts meta.type.declaration.ts storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "Truthy", + "t": "source.ts meta.type.declaration.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "<", + "t": "source.ts meta.type.declaration.ts meta.type.parameters.ts punctuation.definition.typeparameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "source.ts meta.type.declaration.ts meta.type.parameters.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ">", + "t": "source.ts meta.type.declaration.ts meta.type.parameters.ts punctuation.definition.typeparameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.type.declaration.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "source.ts meta.type.declaration.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "extends", + "t": "source.ts meta.type.declaration.ts storage.modifier.ts", + "r": { + "dark_plus": "storage.modifier: #569CD6", + "light_plus": "storage.modifier: #0000FF", + "dark_vs": "storage.modifier: #569CD6", + "light_vs": "storage.modifier: #0000FF", + "hc_black": "storage.modifier: #569CD6", + "dark_modern": "storage.modifier: #569CD6", + "hc_light": "storage.modifier: #0F4A85", + "light_modern": "storage.modifier: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "'", + "t": "source.ts meta.type.declaration.ts string.quoted.single.ts punctuation.definition.string.begin.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "source.ts meta.type.declaration.ts string.quoted.single.ts punctuation.definition.string.end.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "0", + "t": "source.ts meta.type.declaration.ts constant.numeric.decimal.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "false", + "t": "source.ts meta.type.declaration.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "null", + "t": "source.ts meta.type.declaration.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "|", + "t": "source.ts meta.type.declaration.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "undefined", + "t": "source.ts meta.type.declaration.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "?", + "t": "source.ts meta.type.declaration.ts keyword.operator.ternary.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "never", + "t": "source.ts meta.type.declaration.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.type.declaration.ts keyword.operator.ternary.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "source.ts meta.type.declaration.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "const", + "t": "source.ts meta.enum.declaration.ts storage.modifier.ts", + "r": { + "dark_plus": "storage.modifier: #569CD6", + "light_plus": "storage.modifier: #0000FF", + "dark_vs": "storage.modifier: #569CD6", + "light_vs": "storage.modifier: #0000FF", + "hc_black": "storage.modifier: #569CD6", + "dark_modern": "storage.modifier: #569CD6", + "hc_light": "storage.modifier: #0F4A85", + "light_modern": "storage.modifier: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.enum.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "enum", + "t": "source.ts meta.enum.declaration.ts storage.type.enum.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.enum.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "EnumName", + "t": "source.ts meta.enum.declaration.ts entity.name.type.enum.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.enum.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.ts meta.enum.declaration.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.enum.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "one", + "t": "source.ts meta.enum.declaration.ts variable.other.enummember.ts", + "r": { + "dark_plus": "variable.other.enummember: #4FC1FF", + "light_plus": "variable.other.enummember: #0070C1", + "dark_vs": "variable.other.enummember: #B5CEA8", + "light_vs": "variable.other.enummember: #098658", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable.other.enummember: #4FC1FF", + "hc_light": "variable.other.enummember: #02715D", + "light_modern": "variable.other.enummember: #0070C1" + } + }, + { + "c": " ", + "t": "source.ts meta.enum.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.enum.declaration.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.enum.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "1", + "t": "source.ts meta.enum.declaration.ts constant.numeric.decimal.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ",", + "t": "source.ts meta.enum.declaration.ts punctuation.separator.comma.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.ts meta.enum.declaration.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "void", + "t": "source.ts keyword.operator.expression.void.ts", + "r": { + "dark_plus": "keyword.operator.expression: #569CD6", + "light_plus": "keyword.operator.expression: #0000FF", + "dark_vs": "keyword.operator.expression: #569CD6", + "light_vs": "keyword.operator.expression: #0000FF", + "hc_black": "keyword.operator.expression: #569CD6", + "dark_modern": "keyword.operator.expression: #569CD6", + "hc_light": "keyword.operator.expression: #0F4A85", + "light_modern": "keyword.operator.expression: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "0", + "t": "source.ts constant.numeric.decimal.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "source.ts meta.function.ts storage.type.function.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "*", + "t": "source.ts meta.function.ts keyword.generator.asterisk.ts", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6", + "dark_modern": "keyword: #569CD6", + "hc_light": "keyword: #0F4A85", + "light_modern": "keyword: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "makeIterator", + "t": "source.ts meta.function.ts meta.definition.function.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "start", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.function.ts meta.parameters.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "0", + "t": "source.ts meta.function.ts meta.parameters.ts constant.numeric.decimal.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ",", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.separator.parameter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "end", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.function.ts meta.parameters.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "Infinity", + "t": "source.ts meta.function.ts meta.parameters.ts constant.language.infinity.ts", + "r": { + "dark_plus": "constant.language: #569CD6", + "light_plus": "constant.language: #0000FF", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", + "hc_black": "constant.language: #569CD6", + "dark_modern": "constant.language: #569CD6", + "hc_light": "constant.language: #0F4A85", + "light_modern": "constant.language: #0000FF" + } + }, + { + "c": ",", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.separator.parameter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "step", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.function.ts meta.parameters.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "1", + "t": "source.ts meta.function.ts meta.parameters.ts constant.numeric.decimal.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ")", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "source.ts meta.function.ts storage.type.function.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "makeDate", + "t": "source.ts meta.function.ts meta.definition.function.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "timestamp", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ")", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.return.type.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "Date", + "t": "source.ts meta.function.ts meta.return.type.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "source.ts meta.function.ts storage.type.function.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "makeDate", + "t": "source.ts meta.function.ts meta.definition.function.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "m", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.separator.parameter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "d", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.separator.parameter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "y", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ")", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.return.type.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "Date", + "t": "source.ts meta.function.ts meta.return.type.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "source.ts meta.function.ts storage.type.function.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "makeDate", + "t": "source.ts meta.function.ts meta.definition.function.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "mOrTimestamp", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.separator.parameter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "d", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "?", + "t": "source.ts meta.function.ts meta.parameters.ts keyword.operator.optional.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.separator.parameter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "y", + "t": "source.ts meta.function.ts meta.parameters.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "?", + "t": "source.ts meta.function.ts meta.parameters.ts keyword.operator.optional.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "source.ts meta.function.ts meta.parameters.ts meta.type.annotation.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ")", + "t": "source.ts meta.function.ts meta.parameters.ts punctuation.definition.parameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "source.ts meta.function.ts meta.return.type.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "Date", + "t": "source.ts meta.function.ts meta.return.type.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.function.ts meta.return.type.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "source.ts meta.function.ts meta.block.ts punctuation.definition.block.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "source.ts meta.type.declaration.ts storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "StringNumberBooleans", + "t": "source.ts meta.type.declaration.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.type.declaration.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "[", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts meta.brace.square.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "string", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts punctuation.separator.comma.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts punctuation.separator.comma.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "...", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts keyword.operator.rest.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "boolean", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "[]", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts meta.type.tuple.ts meta.brace.square.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "]", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts meta.brace.square.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "source.ts meta.type.declaration.ts storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "StringBooleansNumber", + "t": "source.ts meta.type.declaration.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.type.declaration.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "[", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts meta.brace.square.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "string", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts punctuation.separator.comma.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "...", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts keyword.operator.rest.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "boolean", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "[]", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts meta.type.tuple.ts meta.brace.square.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ",", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts punctuation.separator.comma.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "]", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts meta.brace.square.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "source.ts meta.type.declaration.ts storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "BooleansStringNumber", + "t": "source.ts meta.type.declaration.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ts meta.type.declaration.ts keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "[", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts meta.brace.square.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "...", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts keyword.operator.rest.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "boolean", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "[]", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts meta.type.tuple.ts meta.brace.square.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ",", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts punctuation.separator.comma.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "string", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts punctuation.separator.comma.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": " ", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "]", + "t": "source.ts meta.type.declaration.ts meta.type.tuple.ts meta.brace.square.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "source.ts punctuation.terminator.statement.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + } +] \ No newline at end of file diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_rb.json b/extensions/vscode-colorize-tests/test/colorize-results/test_rb.json index 6230a5f14b3..032617573e4 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_rb.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_rb.json @@ -113,7 +113,7 @@ }, { "c": "module", - "t": "source.ruby keyword.control.ruby", + "t": "source.ruby meta.module.ruby keyword.control.module.ruby", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -127,7 +127,7 @@ }, { "c": " ", - "t": "source.ruby", + "t": "source.ruby meta.module.ruby", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -140,50 +140,22 @@ } }, { - "c": "Azure", - "t": "source.ruby support.class.ruby", + "c": "Azure::ARM", + "t": "source.ruby meta.module.ruby entity.name.type.module.ruby", "r": { - "dark_plus": "support.class: #4EC9B0", - "light_plus": "support.class: #267F99", + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.class: #4EC9B0", - "dark_modern": "support.class: #4EC9B0", - "hc_light": "support.class: #185E73", - "light_modern": "support.class: #267F99" + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" } }, { "c": "::", - "t": "source.ruby punctuation.separator.namespace.ruby", - "r": { - "dark_plus": "punctuation.separator.namespace.ruby: #4EC9B0", - "light_plus": "punctuation.separator.namespace.ruby: #267F99", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "punctuation.separator.namespace.ruby: #4EC9B0", - "dark_modern": "punctuation.separator.namespace.ruby: #4EC9B0", - "hc_light": "punctuation.separator.namespace.ruby: #185E73", - "light_modern": "punctuation.separator.namespace.ruby: #267F99" - } - }, - { - "c": "ARM", - "t": "source.ruby support.class.ruby", - "r": { - "dark_plus": "support.class: #4EC9B0", - "light_plus": "support.class: #267F99", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "support.class: #4EC9B0", - "dark_modern": "support.class: #4EC9B0", - "hc_light": "support.class: #185E73", - "light_modern": "support.class: #267F99" - } - }, - { - "c": "::", - "t": "source.ruby punctuation.separator.namespace.ruby", + "t": "source.ruby meta.module.ruby entity.name.type.module.ruby punctuation.separator.namespace.ruby", "r": { "dark_plus": "punctuation.separator.namespace.ruby: #4EC9B0", "light_plus": "punctuation.separator.namespace.ruby: #267F99", @@ -197,16 +169,16 @@ }, { "c": "Scheduler", - "t": "source.ruby variable.other.constant.ruby", + "t": "source.ruby meta.module.ruby entity.name.type.module.ruby", "r": { - "dark_plus": "variable.other.constant: #4FC1FF", - "light_plus": "variable.other.constant: #0070C1", + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "variable: #9CDCFE", - "dark_modern": "variable.other.constant: #4FC1FF", - "hc_light": "variable.other.constant: #02715D", - "light_modern": "variable.other.constant: #0070C1" + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" } }, { @@ -323,7 +295,7 @@ }, { "c": "class", - "t": "source.ruby keyword.control.ruby", + "t": "source.ruby meta.class.ruby keyword.control.class.ruby", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -337,7 +309,7 @@ }, { "c": " ", - "t": "source.ruby", + "t": "source.ruby meta.class.ruby", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -351,21 +323,21 @@ }, { "c": "SchedulerManagementClient", - "t": "source.ruby variable.other.constant.ruby", + "t": "source.ruby meta.class.ruby entity.name.type.class.ruby", "r": { - "dark_plus": "variable.other.constant: #4FC1FF", - "light_plus": "variable.other.constant: #0070C1", + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "variable: #9CDCFE", - "dark_modern": "variable.other.constant: #4FC1FF", - "hc_light": "variable.other.constant: #02715D", - "light_modern": "variable.other.constant: #0070C1" + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" } }, { "c": " ", - "t": "source.ruby", + "t": "source.ruby meta.class.ruby", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -379,21 +351,21 @@ }, { "c": "<", - "t": "source.ruby keyword.operator.comparison.ruby", + "t": "source.ruby meta.class.ruby punctuation.separator.inheritance.ruby", "r": { - "dark_plus": "keyword.operator: #D4D4D4", - "light_plus": "keyword.operator: #000000", - "dark_vs": "keyword.operator: #D4D4D4", - "light_vs": "keyword.operator: #000000", - "hc_black": "keyword.operator: #D4D4D4", - "dark_modern": "keyword.operator: #D4D4D4", - "hc_light": "keyword.operator: #000000", - "light_modern": "keyword.operator: #000000" + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" } }, { "c": " ", - "t": "source.ruby", + "t": "source.ruby meta.class.ruby", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -407,21 +379,21 @@ }, { "c": "MsRestAzure", - "t": "source.ruby support.class.ruby", + "t": "source.ruby meta.class.ruby entity.other.inherited-class.ruby", "r": { - "dark_plus": "support.class: #4EC9B0", - "light_plus": "support.class: #267F99", + "dark_plus": "entity.other.inherited-class: #4EC9B0", + "light_plus": "entity.other.inherited-class: #267F99", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.class: #4EC9B0", - "dark_modern": "support.class: #4EC9B0", - "hc_light": "support.class: #185E73", - "light_modern": "support.class: #267F99" + "hc_black": "entity.other.inherited-class: #4EC9B0", + "dark_modern": "entity.other.inherited-class: #4EC9B0", + "hc_light": "entity.other.inherited-class: #185E73", + "light_modern": "entity.other.inherited-class: #267F99" } }, { "c": "::", - "t": "source.ruby punctuation.separator.namespace.ruby", + "t": "source.ruby meta.class.ruby entity.other.inherited-class.ruby punctuation.separator.namespace.ruby", "r": { "dark_plus": "punctuation.separator.namespace.ruby: #4EC9B0", "light_plus": "punctuation.separator.namespace.ruby: #267F99", @@ -435,16 +407,16 @@ }, { "c": "AzureServiceClient", - "t": "source.ruby variable.other.constant.ruby", + "t": "source.ruby meta.class.ruby entity.other.inherited-class.ruby", "r": { - "dark_plus": "variable.other.constant: #4FC1FF", - "light_plus": "variable.other.constant: #0070C1", + "dark_plus": "entity.other.inherited-class: #4EC9B0", + "light_plus": "entity.other.inherited-class: #267F99", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "variable: #9CDCFE", - "dark_modern": "variable.other.constant: #4FC1FF", - "hc_light": "variable.other.constant: #02715D", - "light_modern": "variable.other.constant: #0070C1" + "hc_black": "entity.other.inherited-class: #4EC9B0", + "dark_modern": "entity.other.inherited-class: #4EC9B0", + "hc_light": "entity.other.inherited-class: #185E73", + "light_modern": "entity.other.inherited-class: #267F99" } }, { @@ -3318,7 +3290,35 @@ } }, { - "c": " = ", + "c": " ", + "t": "source.ruby", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ruby keyword.operator.assignment.ruby", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", "t": "source.ruby", "r": { "dark_plus": "default: #D4D4D4", @@ -3472,7 +3472,35 @@ } }, { - "c": " = ", + "c": " ", + "t": "source.ruby", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ruby keyword.operator.assignment.ruby", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " ", "t": "source.ruby", "r": { "dark_plus": "default: #D4D4D4", @@ -3584,7 +3612,35 @@ } }, { - "c": " = version", + "c": " ", + "t": "source.ruby", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "source.ruby keyword.operator.assignment.ruby", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": " version", "t": "source.ruby", "r": { "dark_plus": "default: #D4D4D4", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-241001_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-241001_ts.json index e870a69ea3f..a5a8c33bf22 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-241001_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-241001_ts.json @@ -1,7 +1,7 @@ [ { "c": "const", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -15,7 +15,7 @@ }, { "c": "n", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -29,7 +29,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -43,7 +43,7 @@ }, { "c": "null", - "t": "constant.language.null", + "t": "constant.language.null.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -57,7 +57,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": "const", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -85,7 +85,7 @@ }, { "c": "u", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -99,7 +99,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -113,7 +113,7 @@ }, { "c": "undefined", - "t": "constant.language.undefined", + "t": "constant.language.undefined.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -127,7 +127,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -141,7 +141,7 @@ }, { "c": "const", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -155,7 +155,7 @@ }, { "c": "a", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -169,7 +169,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -183,7 +183,7 @@ }, { "c": "true", - "t": "constant.language.boolean.true", + "t": "constant.language.boolean.true.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -197,7 +197,7 @@ }, { "c": "||", - "t": "keyword.operator.logical", + "t": "keyword.operator.logical.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -211,7 +211,7 @@ }, { "c": "false", - "t": "constant.language.boolean.false", + "t": "constant.language.boolean.false.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -225,7 +225,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -239,21 +239,21 @@ }, { "c": "type", - "t": "keyword.control", + "t": "storage.type.type.ts", "r": { - "dark_plus": "keyword.control: #C586C0", - "light_plus": "keyword.control: #AF00DB", - "dark_vs": "keyword.control: #569CD6", - "light_vs": "keyword.control: #0000FF", - "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #C586C0", - "hc_light": "keyword.control: #B5200D", - "light_modern": "keyword.control: #AF00DB" + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" } }, { "c": "T", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.alias.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -267,7 +267,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -281,12 +281,12 @@ }, { "c": "null", - "t": "support.type.builtin", + "t": "constant.language.null.ts support.type.builtin.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", "hc_black": "support.type: #4EC9B0", "dark_modern": "support.type: #4EC9B0", "hc_light": "support.type: #185E73", @@ -295,7 +295,7 @@ }, { "c": "|", - "t": "keyword.operator.type", + "t": "keyword.operator.ts keyword.operator.type.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -309,7 +309,7 @@ }, { "c": "unknown", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -323,7 +323,7 @@ }, { "c": "|", - "t": "keyword.operator.type", + "t": "keyword.operator.ts keyword.operator.type.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -337,12 +337,12 @@ }, { "c": "undefined", - "t": "support.type.builtin", + "t": "constant.language.undefined.ts support.type.builtin.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", "hc_black": "support.type: #4EC9B0", "dark_modern": "support.type: #4EC9B0", "hc_light": "support.type: #185E73", @@ -351,7 +351,7 @@ }, { "c": "|", - "t": "keyword.operator.type", + "t": "keyword.operator.ts keyword.operator.type.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -365,12 +365,12 @@ }, { "c": "true", - "t": "support.type.builtin", + "t": "constant.language.boolean.true.ts support.type.builtin.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", "hc_black": "support.type: #4EC9B0", "dark_modern": "support.type: #4EC9B0", "hc_light": "support.type: #185E73", @@ -379,7 +379,7 @@ }, { "c": "|", - "t": "keyword.operator.type", + "t": "keyword.operator.ts keyword.operator.type.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -393,12 +393,12 @@ }, { "c": "false", - "t": "support.type.builtin", + "t": "constant.language.boolean.false.ts support.type.builtin.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", "hc_black": "support.type: #4EC9B0", "dark_modern": "support.type: #4EC9B0", "hc_light": "support.type: #185E73", @@ -407,7 +407,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -421,7 +421,7 @@ }, { "c": "function", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -435,7 +435,7 @@ }, { "c": "bar", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -449,7 +449,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -463,7 +463,7 @@ }, { "c": "a", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -477,7 +477,7 @@ }, { "c": "?", - "t": "keyword.operator.optional", + "t": "punctuation.delimiter.ts keyword.operator.optional.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -491,7 +491,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -505,7 +505,7 @@ }, { "c": "string", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -519,7 +519,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -533,7 +533,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -547,7 +547,7 @@ }, { "c": "string", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -561,7 +561,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -575,7 +575,7 @@ }, { "c": "return", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -589,7 +589,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -603,7 +603,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -617,7 +617,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -631,7 +631,7 @@ }, { "c": "interface", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -645,7 +645,7 @@ }, { "c": "A", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.interface.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -659,7 +659,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -673,7 +673,7 @@ }, { "c": "b", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -687,7 +687,7 @@ }, { "c": "?", - "t": "keyword.operator.optional", + "t": "punctuation.delimiter.ts keyword.operator.optional.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -701,7 +701,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -715,7 +715,7 @@ }, { "c": "2", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -729,7 +729,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -743,7 +743,7 @@ }, { "c": "a", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -757,7 +757,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -771,7 +771,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -785,7 +785,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -799,7 +799,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -813,7 +813,7 @@ }, { "c": "const", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -827,7 +827,7 @@ }, { "c": "obj", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -841,7 +841,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -855,7 +855,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -869,7 +869,7 @@ }, { "c": "a", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -883,7 +883,7 @@ }, { "c": ":", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -897,7 +897,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -911,7 +911,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -925,7 +925,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -939,7 +939,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -953,7 +953,7 @@ }, { "c": "const", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -967,7 +967,7 @@ }, { "c": "obj1", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -981,7 +981,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -995,7 +995,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1009,7 +1009,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1023,7 +1023,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1037,7 +1037,7 @@ }, { "c": "const", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -1051,7 +1051,7 @@ }, { "c": "obj2", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1065,7 +1065,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1079,7 +1079,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1093,7 +1093,7 @@ }, { "c": "...", - "t": "keyword.operator.spread", + "t": "keyword.operator.spread.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1107,7 +1107,7 @@ }, { "c": "obj1", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1121,7 +1121,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1135,7 +1135,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1149,7 +1149,7 @@ }, { "c": "function", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -1163,7 +1163,7 @@ }, { "c": "foo", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -1177,7 +1177,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1191,7 +1191,7 @@ }, { "c": "param", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1205,7 +1205,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1219,7 +1219,7 @@ }, { "c": "string", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -1233,7 +1233,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1247,7 +1247,7 @@ }, { "c": "...", - "t": "keyword.operator.rest", + "t": "keyword.operator.rest.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1261,7 +1261,7 @@ }, { "c": "rest", - "t": "variable", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1275,7 +1275,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1289,7 +1289,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1303,7 +1303,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1317,7 +1317,7 @@ }, { "c": "undefined", - "t": "constant.language.undefined", + "t": "constant.language.undefined.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -1331,7 +1331,7 @@ }, { "c": "null", - "t": "constant.language.null", + "t": "constant.language.null.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -1345,7 +1345,7 @@ }, { "c": "NaN", - "t": "constant.language.nan", + "t": "variable.ts constant.language.nan.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -1359,7 +1359,7 @@ }, { "c": "Infinity", - "t": "constant.language.infinity", + "t": "variable.ts constant.language.infinity.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -1373,21 +1373,21 @@ }, { "c": "type", - "t": "keyword.control", + "t": "storage.type.type.ts", "r": { - "dark_plus": "keyword.control: #C586C0", - "light_plus": "keyword.control: #AF00DB", - "dark_vs": "keyword.control: #569CD6", - "light_vs": "keyword.control: #0000FF", - "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #C586C0", - "hc_light": "keyword.control: #B5200D", - "light_modern": "keyword.control: #AF00DB" + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" } }, { "c": "One", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.alias.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -1401,7 +1401,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1415,7 +1415,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1429,7 +1429,7 @@ }, { "c": "zcxvf", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1443,7 +1443,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1457,7 +1457,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1471,21 +1471,21 @@ }, { "c": "type", - "t": "keyword.control", + "t": "storage.type.type.ts", "r": { - "dark_plus": "keyword.control: #C586C0", - "light_plus": "keyword.control: #AF00DB", - "dark_vs": "keyword.control: #569CD6", - "light_vs": "keyword.control: #0000FF", - "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #C586C0", - "hc_light": "keyword.control: #B5200D", - "light_modern": "keyword.control: #AF00DB" + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" } }, { "c": "Two", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.alias.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -1499,7 +1499,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1513,7 +1513,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1527,7 +1527,7 @@ }, { "c": "one", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1541,7 +1541,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1555,7 +1555,7 @@ }, { "c": "|", - "t": "keyword.operator.type", + "t": "keyword.operator.ts keyword.operator.type.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1569,7 +1569,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1583,7 +1583,7 @@ }, { "c": "two", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1597,7 +1597,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1611,7 +1611,7 @@ }, { "c": "&", - "t": "keyword.operator.type", + "t": "keyword.operator.ts keyword.operator.type.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1625,7 +1625,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1639,7 +1639,7 @@ }, { "c": "three", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1653,7 +1653,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1667,7 +1667,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1681,7 +1681,7 @@ }, { "c": "const", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -1695,7 +1695,7 @@ }, { "c": "obj", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1709,7 +1709,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1723,7 +1723,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1737,7 +1737,7 @@ }, { "c": "one", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1751,7 +1751,7 @@ }, { "c": ":", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1765,7 +1765,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1779,7 +1779,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1793,7 +1793,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1807,21 +1807,21 @@ }, { "c": "type", - "t": "keyword.control", + "t": "storage.type.type.ts", "r": { - "dark_plus": "keyword.control: #C586C0", - "light_plus": "keyword.control: #AF00DB", - "dark_vs": "keyword.control: #569CD6", - "light_vs": "keyword.control: #0000FF", - "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #C586C0", - "hc_light": "keyword.control: #B5200D", - "light_modern": "keyword.control: #AF00DB" + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" } }, { "c": "Rec", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.alias.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -1835,7 +1835,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1849,7 +1849,7 @@ }, { "c": "Record", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -1863,7 +1863,7 @@ }, { "c": "<", - "t": "punctuation.definition.typeparameters", + "t": "punctuation.definition.typeparameters.begin.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1877,7 +1877,7 @@ }, { "c": "string", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -1891,7 +1891,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1905,7 +1905,7 @@ }, { "c": "string", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -1919,7 +1919,7 @@ }, { "c": ">", - "t": "punctuation.definition.typeparameters", + "t": "punctuation.definition.typeparameters.end.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1933,7 +1933,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1947,7 +1947,7 @@ }, { "c": "new", - "t": "keyword.operator.new", + "t": "new.expr.ts keyword.operator.new.ts", "r": { "dark_plus": "keyword.operator.new: #569CD6", "light_plus": "keyword.operator.new: #0000FF", @@ -1961,21 +1961,21 @@ }, { "c": "URL", - "t": "variable.other.constant", + "t": "new.expr.ts variable.ts entity.name.function.ts", "r": { - "dark_plus": "variable.other.constant: #4FC1FF", - "light_plus": "variable.other.constant: #0070C1", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "variable: #9CDCFE", - "dark_modern": "variable.other.constant: #4FC1FF", - "hc_light": "variable.other.constant: #02715D", - "light_modern": "variable.other.constant: #0070C1" + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" } }, { "c": "(", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1989,7 +1989,7 @@ }, { "c": "'", - "t": "string", + "t": "new.expr.ts string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -2003,7 +2003,7 @@ }, { "c": "./../../renderer/dist/index.html", - "t": "string", + "t": "new.expr.ts string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -2017,7 +2017,7 @@ }, { "c": "'", - "t": "string", + "t": "new.expr.ts string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -2031,7 +2031,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2045,7 +2045,7 @@ }, { "c": "import", - "t": "keyword.control", + "t": "new.expr.ts keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -2059,7 +2059,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2073,7 +2073,7 @@ }, { "c": "meta", - "t": "new.expr", + "t": "new.expr.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2087,7 +2087,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2101,7 +2101,7 @@ }, { "c": "url", - "t": "variable", + "t": "new.expr.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2115,7 +2115,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2129,7 +2129,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-function-inv_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-function-inv_ts.json index e2bc3557ae5..046a20d140a 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-function-inv_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-function-inv_ts.json @@ -1,7 +1,7 @@ [ { "c": "rowData", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -15,7 +15,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -29,7 +29,7 @@ }, { "c": "push", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -43,7 +43,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": "callback", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -71,7 +71,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -85,7 +85,7 @@ }, { "c": "new", - "t": "keyword.operator.new", + "t": "new.expr.ts keyword.operator.new.ts", "r": { "dark_plus": "keyword.operator.new: #569CD6", "light_plus": "keyword.operator.new: #0000FF", @@ -99,7 +99,7 @@ }, { "c": "Cell", - "t": "entity.name.function", + "t": "new.expr.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -113,7 +113,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": "row", - "t": "variable", + "t": "new.expr.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -141,7 +141,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -155,7 +155,7 @@ }, { "c": "col", - "t": "variable", + "t": "new.expr.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -169,7 +169,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -183,7 +183,7 @@ }, { "c": "false", - "t": "constant.language.boolean.false", + "t": "new.expr.ts constant.language.boolean.false.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -197,7 +197,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -211,7 +211,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -225,7 +225,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -239,7 +239,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue11_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue11_ts.json index 9ffdc10c74c..5e8481b6289 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue11_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue11_ts.json @@ -1,7 +1,7 @@ [ { "c": "let", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -15,7 +15,7 @@ }, { "c": "keyCode", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -29,7 +29,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -43,7 +43,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -57,7 +57,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": "if", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -85,7 +85,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -99,7 +99,7 @@ }, { "c": "!", - "t": "keyword.operator.logical", + "t": "keyword.operator.logical.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -113,7 +113,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": "keyCode", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -141,7 +141,7 @@ }, { "c": "===", - "t": "keyword.operator.logical", + "t": "keyword.operator.logical.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -155,7 +155,7 @@ }, { "c": "8", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -169,7 +169,7 @@ }, { "c": "||", - "t": "keyword.operator.logical", + "t": "keyword.operator.logical.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -183,7 +183,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +197,7 @@ }, { "c": "keyCode", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -211,7 +211,7 @@ }, { "c": ">=", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -225,7 +225,7 @@ }, { "c": "48", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -239,7 +239,7 @@ }, { "c": "&&", - "t": "keyword.operator.logical", + "t": "keyword.operator.logical.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -253,7 +253,7 @@ }, { "c": "keyCode", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -267,7 +267,7 @@ }, { "c": "<=", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -281,7 +281,7 @@ }, { "c": "57", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -295,7 +295,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -309,7 +309,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -323,7 +323,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -337,7 +337,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -351,7 +351,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -365,7 +365,7 @@ }, { "c": "for", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -379,7 +379,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -393,7 +393,7 @@ }, { "c": "let", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -407,7 +407,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -421,7 +421,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -435,7 +435,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -449,7 +449,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -463,7 +463,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -477,7 +477,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -491,7 +491,7 @@ }, { "c": "5", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -505,7 +505,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -519,7 +519,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -533,7 +533,7 @@ }, { "c": "++", - "t": "keyword.operator.increment", + "t": "keyword.operator.increment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -547,7 +547,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -561,7 +561,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -575,7 +575,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -589,7 +589,7 @@ }, { "c": "for", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -603,7 +603,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -617,7 +617,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -631,7 +631,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -645,7 +645,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -659,7 +659,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -673,7 +673,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -687,7 +687,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -701,7 +701,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -715,7 +715,7 @@ }, { "c": "5", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -729,7 +729,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -743,7 +743,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -757,7 +757,7 @@ }, { "c": "++", - "t": "keyword.operator.increment", + "t": "keyword.operator.increment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -771,7 +771,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -785,7 +785,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -799,7 +799,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -813,7 +813,7 @@ }, { "c": "for", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -827,7 +827,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -841,7 +841,7 @@ }, { "c": "let", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -855,7 +855,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -869,7 +869,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -883,7 +883,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -897,7 +897,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -911,7 +911,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -925,7 +925,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -939,7 +939,7 @@ }, { "c": "5", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -953,7 +953,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -967,7 +967,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -981,7 +981,7 @@ }, { "c": "++", - "t": "keyword.operator.increment", + "t": "keyword.operator.increment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -995,7 +995,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1009,7 +1009,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1023,7 +1023,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1037,7 +1037,7 @@ }, { "c": "for", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -1051,7 +1051,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1065,7 +1065,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1079,7 +1079,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1093,7 +1093,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1107,7 +1107,7 @@ }, { "c": "5", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1121,7 +1121,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1135,7 +1135,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1149,7 +1149,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1163,7 +1163,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1177,7 +1177,7 @@ }, { "c": "for", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -1191,7 +1191,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1205,7 +1205,7 @@ }, { "c": "let", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -1219,7 +1219,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1233,7 +1233,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1247,7 +1247,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1261,7 +1261,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1275,7 +1275,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1289,7 +1289,7 @@ }, { "c": "+", - "t": "keyword.operator.arithmetic", + "t": "keyword.operator.arithmetic.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1303,7 +1303,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1317,7 +1317,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1331,7 +1331,7 @@ }, { "c": "<<", - "t": "keyword.operator", + "t": "keyword.operator.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1345,7 +1345,7 @@ }, { "c": "5", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1359,7 +1359,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1373,7 +1373,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1387,7 +1387,7 @@ }, { "c": "5", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1401,7 +1401,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1415,7 +1415,7 @@ }, { "c": "i", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1429,7 +1429,7 @@ }, { "c": "++", - "t": "keyword.operator.increment", + "t": "keyword.operator.increment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1443,7 +1443,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1457,7 +1457,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1471,7 +1471,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1485,7 +1485,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -1499,7 +1499,7 @@ }, { "c": "p", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1513,7 +1513,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1527,7 +1527,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1541,7 +1541,7 @@ }, { "c": "?", - "t": "keyword.operator.ternary", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1555,7 +1555,7 @@ }, { "c": "2", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1569,7 +1569,7 @@ }, { "c": ":", - "t": "keyword.operator.ternary", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1583,7 +1583,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1597,7 +1597,7 @@ }, { "c": "3", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1611,7 +1611,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1625,7 +1625,7 @@ }, { "c": "4", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1639,7 +1639,7 @@ }, { "c": "?", - "t": "keyword.operator.ternary", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1653,7 +1653,7 @@ }, { "c": "5", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1667,7 +1667,7 @@ }, { "c": ":", - "t": "keyword.operator.ternary", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1681,7 +1681,7 @@ }, { "c": "6", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1695,7 +1695,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1709,7 +1709,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1723,7 +1723,7 @@ }, { "c": "class", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -1737,7 +1737,7 @@ }, { "c": "A", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.class.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -1751,7 +1751,7 @@ }, { "c": "<", - "t": "", + "t": "punctuation.definition.typeparameters.begin.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1765,7 +1765,7 @@ }, { "c": "X", - "t": "entity.name.type", + "t": "entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -1779,7 +1779,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1793,7 +1793,7 @@ }, { "c": "Y", - "t": "entity.name.type", + "t": "entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -1807,7 +1807,7 @@ }, { "c": ">", - "t": "", + "t": "punctuation.definition.typeparameters.end.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1821,7 +1821,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1835,7 +1835,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1849,7 +1849,7 @@ }, { "c": "class", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -1863,7 +1863,7 @@ }, { "c": "A1", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.class.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -1877,7 +1877,7 @@ }, { "c": "<", - "t": "", + "t": "punctuation.definition.typeparameters.begin.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1891,7 +1891,7 @@ }, { "c": "T", - "t": "entity.name.type", + "t": "entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -1905,7 +1905,7 @@ }, { "c": "extends", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -1919,7 +1919,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1933,7 +1933,7 @@ }, { "c": "a", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1947,7 +1947,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1961,7 +1961,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1975,7 +1975,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1989,7 +1989,7 @@ }, { "c": "=>", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -2003,7 +2003,7 @@ }, { "c": "string", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -2017,7 +2017,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2031,7 +2031,7 @@ }, { "c": ">", - "t": "", + "t": "punctuation.definition.typeparameters.end.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2045,7 +2045,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2059,7 +2059,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2073,7 +2073,7 @@ }, { "c": "class", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -2087,7 +2087,7 @@ }, { "c": "B", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.class.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -2101,7 +2101,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2115,7 +2115,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2129,7 +2129,7 @@ }, { "c": "class", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -2143,7 +2143,7 @@ }, { "c": "C", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.class.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -2157,7 +2157,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2171,7 +2171,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2185,7 +2185,7 @@ }, { "c": "function", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -2199,7 +2199,7 @@ }, { "c": "foo", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -2213,7 +2213,7 @@ }, { "c": "<", - "t": "", + "t": "punctuation.definition.typeparameters.begin.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2227,7 +2227,7 @@ }, { "c": "T", - "t": "entity.name.type", + "t": "entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -2241,7 +2241,7 @@ }, { "c": ">", - "t": "", + "t": "punctuation.definition.typeparameters.end.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2255,7 +2255,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2269,7 +2269,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2283,7 +2283,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2297,7 +2297,7 @@ }, { "c": "return", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -2311,7 +2311,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -2325,7 +2325,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2339,7 +2339,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2353,7 +2353,7 @@ }, { "c": "let", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -2367,7 +2367,7 @@ }, { "c": "x1", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2381,7 +2381,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2395,7 +2395,7 @@ }, { "c": "A", - "t": "entity.name.type", + "t": "entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -2409,7 +2409,7 @@ }, { "c": "<", - "t": "punctuation.definition.typeparameters", + "t": "punctuation.definition.typeparameters.begin.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2423,7 +2423,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2437,7 +2437,7 @@ }, { "c": "param", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2451,7 +2451,7 @@ }, { "c": "?", - "t": "keyword.operator.optional", + "t": "punctuation.delimiter.ts keyword.operator.optional.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2465,7 +2465,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2479,7 +2479,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -2493,7 +2493,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2507,7 +2507,7 @@ }, { "c": "=>", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -2521,7 +2521,7 @@ }, { "c": "void", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -2535,7 +2535,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2549,7 +2549,7 @@ }, { "c": "B", - "t": "entity.name.type", + "t": "entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -2563,7 +2563,7 @@ }, { "c": ">", - "t": "punctuation.definition.typeparameters", + "t": "punctuation.definition.typeparameters.end.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2577,7 +2577,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2591,7 +2591,7 @@ }, { "c": "let", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -2605,7 +2605,7 @@ }, { "c": "x2", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2619,7 +2619,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2633,7 +2633,7 @@ }, { "c": "A", - "t": "entity.name.type", + "t": "entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -2647,7 +2647,7 @@ }, { "c": "<", - "t": "punctuation.definition.typeparameters", + "t": "punctuation.definition.typeparameters.begin.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2661,7 +2661,7 @@ }, { "c": "C", - "t": "entity.name.type", + "t": "entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -2675,7 +2675,7 @@ }, { "c": "|", - "t": "keyword.operator.type", + "t": "keyword.operator.ts keyword.operator.type.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2689,7 +2689,7 @@ }, { "c": "B", - "t": "entity.name.type", + "t": "entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -2703,7 +2703,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2717,7 +2717,7 @@ }, { "c": "C", - "t": "entity.name.type", + "t": "entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -2731,7 +2731,7 @@ }, { "c": "&", - "t": "keyword.operator.type", + "t": "keyword.operator.ts keyword.operator.type.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2745,7 +2745,7 @@ }, { "c": "B", - "t": "entity.name.type", + "t": "entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -2759,7 +2759,7 @@ }, { "c": ">", - "t": "punctuation.definition.typeparameters", + "t": "punctuation.definition.typeparameters.end.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2773,7 +2773,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2787,7 +2787,7 @@ }, { "c": "const", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -2801,7 +2801,7 @@ }, { "c": "t", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2815,7 +2815,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2829,7 +2829,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -2843,7 +2843,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2857,7 +2857,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2871,7 +2871,7 @@ }, { "c": "5", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -2885,7 +2885,7 @@ }, { "c": ">", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2899,7 +2899,7 @@ }, { "c": "10", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -2913,7 +2913,7 @@ }, { "c": "?", - "t": "keyword.operator.ternary", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2927,7 +2927,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -2941,7 +2941,7 @@ }, { "c": ":", - "t": "keyword.operator.ternary", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2955,7 +2955,7 @@ }, { "c": "2", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -2969,7 +2969,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2983,7 +2983,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2997,7 +2997,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -3011,7 +3011,7 @@ }, { "c": "f6", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -3025,7 +3025,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -3039,7 +3039,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -3053,7 +3053,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -3067,7 +3067,7 @@ }, { "c": "foo", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -3081,7 +3081,7 @@ }, { "c": "<", - "t": "punctuation.definition.typeparameters", + "t": "punctuation.definition.typeparameters.begin.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3095,7 +3095,7 @@ }, { "c": "string", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -3109,7 +3109,7 @@ }, { "c": ">", - "t": "punctuation.definition.typeparameters", + "t": "punctuation.definition.typeparameters.end.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3123,7 +3123,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3137,7 +3137,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3151,7 +3151,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue241715_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue241715_ts.json new file mode 100644 index 00000000000..6d4254d3ab7 --- /dev/null +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue241715_ts.json @@ -0,0 +1,4692 @@ +[ + { + "c": "type", + "t": "storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "obj", + "t": "entity.name.type.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "[", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "key", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "string", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "]", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "any", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "destruct", + "t": "variable.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "start", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "end", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "message", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "any", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "any", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "return", + "t": "keyword.control.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": "start", + "t": "variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "+", + "t": "keyword.operator.arithmetic.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "end", + "t": "variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "+", + "t": "keyword.operator.arithmetic.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "message", + "t": "variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "destructArr", + "t": "variable.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "[", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "first", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "second", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "]", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "any", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "any", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "return", + "t": "keyword.control.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": "first", + "t": "variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "+", + "t": "keyword.operator.arithmetic.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "second", + "t": "variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "interface", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "WebviewMessageUpdateEverything", + "t": "entity.name.type.ts entity.name.type.interface.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "extends", + "t": "storage.modifier.ts", + "r": { + "dark_plus": "storage.modifier: #569CD6", + "light_plus": "storage.modifier: #0000FF", + "dark_vs": "storage.modifier: #569CD6", + "light_vs": "storage.modifier: #0000FF", + "hc_black": "storage.modifier: #569CD6", + "dark_modern": "storage.modifier: #569CD6", + "hc_light": "storage.modifier: #0F4A85", + "light_modern": "storage.modifier: #0000FF" + } + }, + { + "c": "WebviewMessageBase", + "t": "entity.name.type.ts entity.other.inherited-class.ts", + "r": { + "dark_plus": "entity.other.inherited-class: #4EC9B0", + "light_plus": "entity.other.inherited-class: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.other.inherited-class: #4EC9B0", + "dark_modern": "entity.other.inherited-class: #4EC9B0", + "hc_light": "entity.other.inherited-class: #185E73", + "light_modern": "entity.other.inherited-class: #267F99" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "NegNum", + "t": "entity.name.type.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "-", + "t": "keyword.operator.arithmetic.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "1", + "t": "constant.numeric.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "-", + "t": "keyword.operator.arithmetic.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "2", + "t": "constant.numeric.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "-", + "t": "keyword.operator.arithmetic.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "10", + "t": "constant.numeric.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "const", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "n", + "t": "variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "NegNum", + "t": "entity.name.type.ts meta.type.annotation.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "-", + "t": "keyword.operator.arithmetic.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "10", + "t": "constant.numeric.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "export", + "t": "keyword.control.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": "interface", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "OptionalMethod", + "t": "entity.name.type.ts entity.name.type.interface.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "optMeth", + "t": "variable.ts meta.definition.method.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "?", + "t": "punctuation.delimiter.ts keyword.operator.optional.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "any", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "window", + "t": "variable.ts variable.other.object.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "addEventListener", + "t": "variable.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "'", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "message", + "t": "string.quoted.single.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "event", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "=>", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "return", + "t": "keyword.control.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": "event", + "t": "variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "export", + "t": "keyword.control.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": "function", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "dayOfTheWeek", + "t": "variable.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "date", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "dayjs", + "t": "meta.type.annotation.ts entity.name.type.ts variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".", + "t": "meta.type.annotation.ts entity.name.type.ts punctuation.delimiter.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "Dayjs", + "t": "meta.type.annotation.ts entity.name.type.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "string", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "return", + "t": "keyword.control.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": "date", + "t": "variable.ts variable.other.object.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ".", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "format", + "t": "variable.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "'", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "ddd", + "t": "string.quoted.single.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "N", + "t": "entity.name.type.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "never", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "any", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "unknown", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "Truthy", + "t": "entity.name.type.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "<", + "t": "punctuation.definition.typeparameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ">", + "t": "punctuation.definition.typeparameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "T", + "t": "entity.name.type.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "extends", + "t": "storage.modifier.ts", + "r": { + "dark_plus": "storage.modifier: #569CD6", + "light_plus": "storage.modifier: #0000FF", + "dark_vs": "storage.modifier: #569CD6", + "light_vs": "storage.modifier: #0000FF", + "hc_black": "storage.modifier: #569CD6", + "dark_modern": "storage.modifier: #569CD6", + "hc_light": "storage.modifier: #0F4A85", + "light_modern": "storage.modifier: #0000FF" + } + }, + { + "c": "'", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts punctuation.definition.string.end.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts punctuation.definition.string.end.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "0", + "t": "constant.numeric.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "false", + "t": "constant.language.boolean.false.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "null", + "t": "constant.language.null.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "undefined", + "t": "constant.language.undefined.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "?", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "never", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "T", + "t": "entity.name.type.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "export", + "t": "keyword.control.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": "function", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "guardedBoolean", + "t": "variable.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "<", + "t": "punctuation.definition.typeparameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ">", + "t": "punctuation.definition.typeparameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "value", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "T", + "t": "entity.name.type.ts meta.type.annotation.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "value", + "t": "variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "is", + "t": "keyword.operator.expression.is.ts", + "r": { + "dark_plus": "keyword.operator.expression: #569CD6", + "light_plus": "keyword.operator.expression: #0000FF", + "dark_vs": "keyword.operator.expression: #569CD6", + "light_vs": "keyword.operator.expression: #0000FF", + "hc_black": "keyword.operator.expression: #569CD6", + "dark_modern": "keyword.operator.expression: #569CD6", + "hc_light": "keyword.operator.expression: #0F4A85", + "light_modern": "keyword.operator.expression: #0000FF" + } + }, + { + "c": "Truthy", + "t": "entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "<", + "t": "punctuation.definition.typeparameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ">", + "t": "punctuation.definition.typeparameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "return", + "t": "keyword.control.ts", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "dark_modern": "keyword.control: #C586C0", + "hc_light": "keyword.control: #B5200D", + "light_modern": "keyword.control: #AF00DB" + } + }, + { + "c": "Boolean", + "t": "variable.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "value", + "t": "variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "Truthy", + "t": "entity.name.type.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "<", + "t": "punctuation.definition.typeparameters.begin.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "T", + "t": "entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ">", + "t": "punctuation.definition.typeparameters.end.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "T", + "t": "entity.name.type.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "extends", + "t": "storage.modifier.ts", + "r": { + "dark_plus": "storage.modifier: #569CD6", + "light_plus": "storage.modifier: #0000FF", + "dark_vs": "storage.modifier: #569CD6", + "light_vs": "storage.modifier: #0000FF", + "hc_black": "storage.modifier: #569CD6", + "dark_modern": "storage.modifier: #569CD6", + "hc_light": "storage.modifier: #0F4A85", + "light_modern": "storage.modifier: #0000FF" + } + }, + { + "c": "'", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts punctuation.definition.string.end.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "'", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts punctuation.definition.string.end.ts", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "0", + "t": "constant.numeric.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "false", + "t": "constant.language.boolean.false.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "null", + "t": "constant.language.null.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "|", + "t": "keyword.operator.ts keyword.operator.type.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "undefined", + "t": "constant.language.undefined.ts support.type.builtin.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "?", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "never", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "T", + "t": "entity.name.type.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "const", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "enum", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "EnumName", + "t": "variable.ts entity.name.type.enum.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "one", + "t": "variable.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "1", + "t": "constant.numeric.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "void", + "t": "keyword.operator.expression.void.ts", + "r": { + "dark_plus": "keyword.operator.expression: #569CD6", + "light_plus": "keyword.operator.expression: #0000FF", + "dark_vs": "keyword.operator.expression: #569CD6", + "light_vs": "keyword.operator.expression: #0000FF", + "hc_black": "keyword.operator.expression: #569CD6", + "dark_modern": "keyword.operator.expression: #569CD6", + "hc_light": "keyword.operator.expression: #0F4A85", + "light_modern": "keyword.operator.expression: #0000FF" + } + }, + { + "c": "0", + "t": "constant.numeric.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "*", + "t": "keyword.generator.asterisk.ts", + "r": { + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6", + "dark_modern": "keyword: #569CD6", + "hc_light": "keyword: #0F4A85", + "light_modern": "keyword: #0000FF" + } + }, + { + "c": "makeIterator", + "t": "variable.ts meta.definition.function.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "start", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "0", + "t": "constant.numeric.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "end", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "Infinity", + "t": "variable.ts variable.parameter.ts constant.language.infinity.ts", + "r": { + "dark_plus": "constant.language: #569CD6", + "light_plus": "constant.language: #0000FF", + "dark_vs": "constant.language: #569CD6", + "light_vs": "constant.language: #0000FF", + "hc_black": "constant.language: #569CD6", + "dark_modern": "constant.language: #569CD6", + "hc_light": "constant.language: #0F4A85", + "light_modern": "constant.language: #0000FF" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "step", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "1", + "t": "constant.numeric.ts", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "dark_modern": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #096D48", + "light_modern": "constant.numeric: #098658" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "makeDate", + "t": "variable.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "timestamp", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "number", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "Date", + "t": "entity.name.type.ts meta.type.annotation.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "makeDate", + "t": "variable.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "m", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "number", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "d", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "number", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "y", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "number", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "Date", + "t": "entity.name.type.ts meta.type.annotation.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "function", + "t": "storage.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "makeDate", + "t": "variable.ts entity.name.function.ts", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" + } + }, + { + "c": "(", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "mOrTimestamp", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "number", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "d", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "?", + "t": "punctuation.delimiter.ts keyword.operator.optional.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "number", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "y", + "t": "variable.ts variable.parameter.ts", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "dark_modern": "variable: #9CDCFE", + "hc_light": "variable: #001080", + "light_modern": "variable: #001080" + } + }, + { + "c": "?", + "t": "punctuation.delimiter.ts keyword.operator.optional.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "number", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ")", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ":", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "Date", + "t": "entity.name.type.ts meta.type.annotation.ts entity.name.type.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "{", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "}", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "StringNumberBooleans", + "t": "entity.name.type.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "[", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "string", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "...", + "t": "keyword.operator.rest.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "boolean", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "[", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "]", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "]", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "StringBooleansNumber", + "t": "entity.name.type.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "[", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "string", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "...", + "t": "keyword.operator.rest.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "boolean", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "[", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "]", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "]", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "type", + "t": "storage.type.type.ts", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" + } + }, + { + "c": "BooleansStringNumber", + "t": "entity.name.type.ts entity.name.type.alias.ts", + "r": { + "dark_plus": "entity.name.type: #4EC9B0", + "light_plus": "entity.name.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.type: #4EC9B0", + "dark_modern": "entity.name.type: #4EC9B0", + "hc_light": "entity.name.type: #185E73", + "light_modern": "entity.name.type: #267F99" + } + }, + { + "c": "=", + "t": "keyword.operator.assignment.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "[", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "...", + "t": "keyword.operator.rest.ts", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" + } + }, + { + "c": "boolean", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "[", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "]", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "string", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": ",", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": "number", + "t": "support.type.ts support.type.primitive.ts", + "r": { + "dark_plus": "support.type: #4EC9B0", + "light_plus": "support.type: #267F99", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "support.type: #4EC9B0", + "dark_modern": "support.type: #4EC9B0", + "hc_light": "support.type: #185E73", + "light_modern": "support.type: #267F99" + } + }, + { + "c": "]", + "t": "punctuation.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + }, + { + "c": ";", + "t": "punctuation.delimiter.ts", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "dark_modern": "default: #CCCCCC", + "hc_light": "default: #292929", + "light_modern": "default: #3B3B3B" + } + } +] \ No newline at end of file diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5431_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5431_ts.json index 4e5c11e475e..a3ed9208103 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5431_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5431_ts.json @@ -1,7 +1,7 @@ [ { "c": "function", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -15,7 +15,7 @@ }, { "c": "foo", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -29,7 +29,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -43,7 +43,7 @@ }, { "c": "isAll", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -57,7 +57,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": "startTime", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -85,7 +85,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -99,7 +99,7 @@ }, { "c": "endTime", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -113,7 +113,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -141,7 +141,7 @@ }, { "c": "const", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -155,7 +155,7 @@ }, { "c": "timeRange", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -169,7 +169,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -183,7 +183,7 @@ }, { "c": "isAll", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -197,7 +197,7 @@ }, { "c": "?", - "t": "keyword.operator.ternary", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -211,7 +211,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -225,7 +225,7 @@ }, { "c": "所有时间", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -239,7 +239,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -253,7 +253,7 @@ }, { "c": ":", - "t": "keyword.operator.ternary", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -267,7 +267,7 @@ }, { "c": "`", - "t": "string", + "t": "string.template.ts punctuation.definition.string.template.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -281,7 +281,7 @@ }, { "c": "${", - "t": "punctuation.definition.template-expression.begin", + "t": "string.template.ts punctuation.definition.template-expression.begin.ts", "r": { "dark_plus": "punctuation.definition.template-expression.begin: #569CD6", "light_plus": "punctuation.definition.template-expression.begin: #0000FF", @@ -295,12 +295,12 @@ }, { "c": "startTime", - "t": "variable", + "t": "string.template.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -309,7 +309,7 @@ }, { "c": "}", - "t": "punctuation.definition.template-expression.end", + "t": "string.template.ts punctuation.ts punctuation.definition.template-expression.end.ts", "r": { "dark_plus": "punctuation.definition.template-expression.end: #569CD6", "light_plus": "punctuation.definition.template-expression.end: #0000FF", @@ -323,7 +323,7 @@ }, { "c": " - ", - "t": "string", + "t": "string.template.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -337,7 +337,7 @@ }, { "c": "${", - "t": "punctuation.definition.template-expression.begin", + "t": "string.template.ts punctuation.definition.template-expression.begin.ts", "r": { "dark_plus": "punctuation.definition.template-expression.begin: #569CD6", "light_plus": "punctuation.definition.template-expression.begin: #0000FF", @@ -351,12 +351,12 @@ }, { "c": "endTime", - "t": "variable", + "t": "string.template.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -365,7 +365,7 @@ }, { "c": "}", - "t": "punctuation.definition.template-expression.end", + "t": "string.template.ts punctuation.ts punctuation.definition.template-expression.end.ts", "r": { "dark_plus": "punctuation.definition.template-expression.end: #569CD6", "light_plus": "punctuation.definition.template-expression.end: #0000FF", @@ -379,7 +379,7 @@ }, { "c": "`", - "t": "string", + "t": "string.template.ts punctuation.definition.string.template.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -393,7 +393,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -407,7 +407,7 @@ }, { "c": "return", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -421,7 +421,7 @@ }, { "c": "true", - "t": "constant.language.boolean.true", + "t": "constant.language.boolean.true.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -435,7 +435,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -449,7 +449,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5465_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5465_ts.json index a7feac02ad5..100bf5c3d07 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5465_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5465_ts.json @@ -1,7 +1,7 @@ [ { "c": "function", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -15,35 +15,35 @@ }, { "c": "*", - "t": "", + "t": "keyword.generator.asterisk.ts", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_plus": "keyword: #569CD6", + "light_plus": "keyword: #0000FF", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword: #569CD6", + "dark_modern": "keyword: #569CD6", + "hc_light": "keyword: #0F4A85", + "light_modern": "keyword: #0000FF" } }, { "c": "foo2", - "t": "variable", + "t": "variable.ts meta.definition.function.ts entity.name.function.ts", "r": { - "dark_plus": "variable: #9CDCFE", - "light_plus": "variable: #001080", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "variable: #9CDCFE", - "dark_modern": "variable: #9CDCFE", - "hc_light": "variable: #001080", - "light_modern": "variable: #001080" + "hc_black": "entity.name.function: #DCDCAA", + "dark_modern": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #5E2CBC", + "light_modern": "entity.name.function: #795E26" } }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -85,7 +85,7 @@ }, { "c": "yield", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -99,7 +99,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -113,7 +113,7 @@ }, { "c": "bar", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -127,7 +127,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -141,7 +141,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -155,7 +155,7 @@ }, { "c": "yield", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -183,7 +183,7 @@ }, { "c": "[", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +197,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -211,7 +211,7 @@ }, { "c": "bar", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -225,7 +225,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -239,7 +239,7 @@ }, { "c": "]", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -253,7 +253,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +267,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5566_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5566_ts.json index 0f181311279..c1a632297c2 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5566_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5566_ts.json @@ -1,7 +1,7 @@ [ { "c": "function", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -15,7 +15,7 @@ }, { "c": "foo3", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -29,7 +29,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -43,7 +43,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": "const", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -85,7 +85,7 @@ }, { "c": "foo", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -99,7 +99,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -113,7 +113,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -141,7 +141,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -155,7 +155,7 @@ }, { "c": "any", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -169,7 +169,7 @@ }, { "c": "=>", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -183,7 +183,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +197,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -211,7 +211,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -225,7 +225,7 @@ }, { "c": "bar", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -239,7 +239,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -253,7 +253,7 @@ }, { "c": ":", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +267,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -281,7 +281,7 @@ }, { "c": "baz", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -295,7 +295,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -309,7 +309,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -323,7 +323,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -337,7 +337,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-jsdoc-multiline-type_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-jsdoc-multiline-type_ts.json index c10c6d0f46e..32a1169366f 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-jsdoc-multiline-type_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-jsdoc-multiline-type_ts.json @@ -1,7 +1,7 @@ [ { "c": "/**\n * @typedef {{\n * id: number,\n * fn: !Function,\n * context: (!Object|undefined)\n * }}\n * @private\n */", - "t": "comment", + "t": "comment.ts", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -15,7 +15,7 @@ }, { "c": "goog", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -29,7 +29,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -43,7 +43,7 @@ }, { "c": "dom", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -57,7 +57,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": "animationFrame", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -85,7 +85,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -99,7 +99,7 @@ }, { "c": "Task_", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -113,7 +113,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": "/**\n * @typedef {{\n * measureTask: goog.dom.animationFrame.Task_,\n * mutateTask: goog.dom.animationFrame.Task_,\n * state: (!Object|undefined),\n * args: (!Array|undefined),\n * isScheduled: boolean\n * }}\n * @private\n */", - "t": "comment", + "t": "comment.ts", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -141,7 +141,7 @@ }, { "c": "goog", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -155,7 +155,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -169,7 +169,7 @@ }, { "c": "dom", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -183,7 +183,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -197,7 +197,7 @@ }, { "c": "animationFrame", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -211,7 +211,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -225,7 +225,7 @@ }, { "c": "TaskSet_", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -239,7 +239,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-keywords_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-keywords_ts.json index 005ae69ef88..6753ae3fa4c 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-keywords_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-keywords_ts.json @@ -1,7 +1,7 @@ [ { "c": "export", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -15,7 +15,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -29,7 +29,7 @@ }, { "c": "foo", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -43,7 +43,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -57,7 +57,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -85,7 +85,7 @@ }, { "c": "=>", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -99,7 +99,7 @@ }, { "c": "new", - "t": "keyword.operator.new", + "t": "new.expr.ts keyword.operator.new.ts", "r": { "dark_plus": "keyword.operator.new: #569CD6", "light_plus": "keyword.operator.new: #0000FF", @@ -113,7 +113,7 @@ }, { "c": "RegExp", - "t": "entity.name.function", + "t": "new.expr.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -127,7 +127,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -141,7 +141,7 @@ }, { "c": "'", - "t": "string", + "t": "new.expr.ts string.quoted.single.ts punctuation.definition.string.begin.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -155,7 +155,7 @@ }, { "c": "'", - "t": "string", + "t": "new.expr.ts string.quoted.single.ts punctuation.definition.string.begin.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -169,7 +169,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -183,7 +183,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-members_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-members_ts.json index a34e098b59d..e57fa0af7ff 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-members_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-members_ts.json @@ -1,7 +1,7 @@ [ { "c": "class", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -15,7 +15,7 @@ }, { "c": "A2", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.class.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -29,7 +29,7 @@ }, { "c": "extends", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -43,7 +43,7 @@ }, { "c": "B1", - "t": "entity.other.inherited-class", + "t": "variable.ts entity.other.inherited-class.ts", "r": { "dark_plus": "entity.other.inherited-class: #4EC9B0", "light_plus": "entity.other.inherited-class: #267F99", @@ -57,7 +57,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -71,7 +71,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -85,7 +85,7 @@ }, { "c": "count", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -99,7 +99,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -113,7 +113,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -127,7 +127,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -141,7 +141,7 @@ }, { "c": "9", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -155,7 +155,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -169,7 +169,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -183,7 +183,7 @@ }, { "c": "resolveNextGeneration", - "t": "entity.name.function", + "t": "variable.ts meta.definition.method.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -197,7 +197,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -211,7 +211,7 @@ }, { "c": "cell", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -225,7 +225,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -239,7 +239,7 @@ }, { "c": "A2", - "t": "entity.name.type", + "t": "entity.name.type.ts meta.type.annotation.ts entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -253,7 +253,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +267,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -281,7 +281,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -295,7 +295,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-object-literals_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-object-literals_ts.json index 27671c9cc24..ecc84c7516e 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-object-literals_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-object-literals_ts.json @@ -1,7 +1,7 @@ [ { "c": "let", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -15,7 +15,7 @@ }, { "c": "s1", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -29,7 +29,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -43,7 +43,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": "k", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -71,7 +71,7 @@ }, { "c": ":", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -85,7 +85,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -99,7 +99,7 @@ }, { "c": "k1", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -113,7 +113,7 @@ }, { "c": ":", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -127,7 +127,7 @@ }, { "c": "s", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -141,7 +141,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -155,7 +155,7 @@ }, { "c": "k2", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -169,7 +169,7 @@ }, { "c": ":", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -183,7 +183,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -197,7 +197,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -211,7 +211,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -225,7 +225,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-strings_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-strings_ts.json index 58210899070..15d340449e4 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-strings_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-strings_ts.json @@ -1,7 +1,7 @@ [ { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -15,7 +15,7 @@ }, { "c": "x", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -29,7 +29,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -43,7 +43,7 @@ }, { "c": "`", - "t": "string", + "t": "string.template.ts punctuation.definition.string.template.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -57,7 +57,7 @@ }, { "c": "Hello ", - "t": "string", + "t": "string.template.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -71,7 +71,7 @@ }, { "c": "${", - "t": "punctuation.definition.template-expression.begin", + "t": "string.template.ts punctuation.definition.template-expression.begin.ts", "r": { "dark_plus": "punctuation.definition.template-expression.begin: #569CD6", "light_plus": "punctuation.definition.template-expression.begin: #0000FF", @@ -85,12 +85,12 @@ }, { "c": "foo", - "t": "variable", + "t": "string.template.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -99,7 +99,7 @@ }, { "c": "}", - "t": "punctuation.definition.template-expression.end", + "t": "string.template.ts punctuation.ts punctuation.definition.template-expression.end.ts", "r": { "dark_plus": "punctuation.definition.template-expression.end: #569CD6", "light_plus": "punctuation.definition.template-expression.end: #0000FF", @@ -113,7 +113,7 @@ }, { "c": "!", - "t": "string", + "t": "string.template.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -127,7 +127,7 @@ }, { "c": "`", - "t": "string", + "t": "string.template.ts punctuation.definition.string.template.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -141,7 +141,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -155,7 +155,7 @@ }, { "c": "console", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -169,7 +169,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -183,7 +183,7 @@ }, { "c": "log", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -197,7 +197,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -211,7 +211,7 @@ }, { "c": "`", - "t": "string", + "t": "string.template.ts punctuation.definition.string.template.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -225,7 +225,7 @@ }, { "c": "string text line 1\nstring text line 2", - "t": "string", + "t": "string.template.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -239,7 +239,7 @@ }, { "c": "`", - "t": "string", + "t": "string.template.ts punctuation.definition.string.template.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -253,7 +253,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -267,7 +267,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -281,7 +281,7 @@ }, { "c": "x", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -295,7 +295,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -309,7 +309,7 @@ }, { "c": "tag", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -323,7 +323,7 @@ }, { "c": "`", - "t": "string", + "t": "string.template.ts punctuation.definition.string.template.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -337,7 +337,7 @@ }, { "c": "Hello ", - "t": "string", + "t": "string.template.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -351,7 +351,7 @@ }, { "c": "${", - "t": "punctuation.definition.template-expression.begin", + "t": "string.template.ts punctuation.definition.template-expression.begin.ts", "r": { "dark_plus": "punctuation.definition.template-expression.begin: #569CD6", "light_plus": "punctuation.definition.template-expression.begin: #0000FF", @@ -365,12 +365,12 @@ }, { "c": "a", - "t": "variable", + "t": "string.template.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -379,7 +379,7 @@ }, { "c": "+", - "t": "keyword.operator.arithmetic", + "t": "string.template.ts keyword.operator.arithmetic.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -393,12 +393,12 @@ }, { "c": "b", - "t": "variable", + "t": "string.template.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -407,7 +407,7 @@ }, { "c": "}", - "t": "punctuation.definition.template-expression.end", + "t": "string.template.ts punctuation.ts punctuation.definition.template-expression.end.ts", "r": { "dark_plus": "punctuation.definition.template-expression.end: #569CD6", "light_plus": "punctuation.definition.template-expression.end: #0000FF", @@ -421,7 +421,7 @@ }, { "c": " world ", - "t": "string", + "t": "string.template.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -435,7 +435,7 @@ }, { "c": "${", - "t": "punctuation.definition.template-expression.begin", + "t": "string.template.ts punctuation.definition.template-expression.begin.ts", "r": { "dark_plus": "punctuation.definition.template-expression.begin: #569CD6", "light_plus": "punctuation.definition.template-expression.begin: #0000FF", @@ -449,12 +449,12 @@ }, { "c": "a", - "t": "variable", + "t": "string.template.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -463,7 +463,7 @@ }, { "c": "*", - "t": "keyword.operator.arithmetic", + "t": "string.template.ts keyword.operator.arithmetic.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -477,12 +477,12 @@ }, { "c": "b", - "t": "variable", + "t": "string.template.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", "hc_black": "variable: #9CDCFE", "dark_modern": "variable: #9CDCFE", "hc_light": "variable: #001080", @@ -491,7 +491,7 @@ }, { "c": "}", - "t": "punctuation.definition.template-expression.end", + "t": "string.template.ts punctuation.ts punctuation.definition.template-expression.end.ts", "r": { "dark_plus": "punctuation.definition.template-expression.end: #569CD6", "light_plus": "punctuation.definition.template-expression.end: #0000FF", @@ -505,7 +505,7 @@ }, { "c": "`", - "t": "string", + "t": "string.template.ts punctuation.definition.string.template.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -519,7 +519,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-this_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-this_ts.json index 4583b657363..aa495a54f95 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-this_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-this_ts.json @@ -1,7 +1,7 @@ [ { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -15,7 +15,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -29,7 +29,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -43,7 +43,7 @@ }, { "c": "foo", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -57,7 +57,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -71,7 +71,7 @@ }, { "c": "9", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -85,7 +85,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -99,7 +99,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_ts.json index 8a0ca583697..b31e6daa154 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_ts.json @@ -1,7 +1,7 @@ [ { "c": "/* Game of Life\n * Implemented in TypeScript\n * To learn more about TypeScript, please visit http://www.typescriptlang.org/\n */", - "t": "comment", + "t": "comment.ts", "r": { "dark_plus": "comment: #6A9955", "light_plus": "comment: #008000", @@ -15,21 +15,21 @@ }, { "c": "module", - "t": "", + "t": "storage.type.namespace.ts", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6", + "dark_modern": "storage.type: #569CD6", + "hc_light": "storage.type: #0F4A85", + "light_modern": "storage.type: #0000FF" } }, { "c": "Conway", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -43,7 +43,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -57,7 +57,7 @@ }, { "c": "export", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -71,7 +71,7 @@ }, { "c": "class", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -85,7 +85,7 @@ }, { "c": "Cell", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.class.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -99,7 +99,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -113,7 +113,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -127,7 +127,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -141,7 +141,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -155,7 +155,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -169,7 +169,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -183,7 +183,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -197,7 +197,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -211,7 +211,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -225,7 +225,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -239,7 +239,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -253,7 +253,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -267,7 +267,7 @@ }, { "c": "live", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -281,7 +281,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -295,7 +295,7 @@ }, { "c": "boolean", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -309,7 +309,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -323,7 +323,7 @@ }, { "c": "constructor", - "t": "storage.type", + "t": "variable.ts meta.definition.method.ts storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -337,7 +337,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -351,7 +351,7 @@ }, { "c": "row", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -365,7 +365,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -379,7 +379,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -393,7 +393,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -407,7 +407,7 @@ }, { "c": "col", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -421,7 +421,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -435,7 +435,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -449,7 +449,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -463,7 +463,7 @@ }, { "c": "live", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -477,7 +477,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -491,7 +491,7 @@ }, { "c": "boolean", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -505,7 +505,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -519,7 +519,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -533,7 +533,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -547,7 +547,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -561,7 +561,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -575,7 +575,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -589,7 +589,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -603,7 +603,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -617,7 +617,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -631,7 +631,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -645,7 +645,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -659,7 +659,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -673,7 +673,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -687,7 +687,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -701,7 +701,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -715,7 +715,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -729,7 +729,7 @@ }, { "c": "live", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -743,7 +743,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -757,7 +757,7 @@ }, { "c": "live", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -771,7 +771,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -785,7 +785,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -799,7 +799,7 @@ }, { "c": "export", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -813,7 +813,7 @@ }, { "c": "class", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -827,7 +827,7 @@ }, { "c": "GameOfLife", - "t": "entity.name.type", + "t": "entity.name.type.ts entity.name.type.class.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -841,7 +841,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -855,7 +855,7 @@ }, { "c": "private", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -869,7 +869,7 @@ }, { "c": "gridSize", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -883,7 +883,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -897,7 +897,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -911,7 +911,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -925,7 +925,7 @@ }, { "c": "private", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -939,7 +939,7 @@ }, { "c": "canvasSize", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -953,7 +953,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -967,7 +967,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -981,7 +981,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -995,7 +995,7 @@ }, { "c": "private", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -1009,7 +1009,7 @@ }, { "c": "lineColor", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1023,7 +1023,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1037,7 +1037,7 @@ }, { "c": "string", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -1051,7 +1051,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1065,7 +1065,7 @@ }, { "c": "private", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -1079,7 +1079,7 @@ }, { "c": "liveColor", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1093,7 +1093,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1107,7 +1107,7 @@ }, { "c": "string", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -1121,7 +1121,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1135,7 +1135,7 @@ }, { "c": "private", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -1149,7 +1149,7 @@ }, { "c": "deadColor", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1163,7 +1163,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1177,7 +1177,7 @@ }, { "c": "string", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -1191,7 +1191,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1205,7 +1205,7 @@ }, { "c": "private", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -1219,7 +1219,7 @@ }, { "c": "initialLifeProbability", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1233,7 +1233,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1247,7 +1247,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -1261,7 +1261,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1275,7 +1275,7 @@ }, { "c": "private", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -1289,7 +1289,7 @@ }, { "c": "animationRate", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1303,7 +1303,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1317,7 +1317,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -1331,7 +1331,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1345,7 +1345,7 @@ }, { "c": "private", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -1359,7 +1359,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1373,7 +1373,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1387,7 +1387,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -1401,7 +1401,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1415,7 +1415,7 @@ }, { "c": "private", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -1429,7 +1429,7 @@ }, { "c": "world", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1443,7 +1443,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1457,7 +1457,7 @@ }, { "c": "constructor", - "t": "storage.type", + "t": "variable.ts meta.definition.method.ts storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -1471,7 +1471,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1485,7 +1485,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1499,7 +1499,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1513,7 +1513,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -1527,7 +1527,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1541,7 +1541,7 @@ }, { "c": "gridSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1555,7 +1555,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1569,7 +1569,7 @@ }, { "c": "50", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1583,7 +1583,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1597,7 +1597,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -1611,7 +1611,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1625,7 +1625,7 @@ }, { "c": "canvasSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1639,7 +1639,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1653,7 +1653,7 @@ }, { "c": "600", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -1667,7 +1667,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1681,7 +1681,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -1695,7 +1695,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1709,7 +1709,7 @@ }, { "c": "lineColor", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1723,7 +1723,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1737,7 +1737,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1751,7 +1751,7 @@ }, { "c": "#cdcdcd", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1765,7 +1765,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1779,7 +1779,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1793,7 +1793,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -1807,7 +1807,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1821,7 +1821,7 @@ }, { "c": "liveColor", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1835,7 +1835,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1849,7 +1849,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1863,7 +1863,7 @@ }, { "c": "#666", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1877,7 +1877,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1891,7 +1891,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1905,7 +1905,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -1919,7 +1919,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1933,7 +1933,7 @@ }, { "c": "deadColor", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -1947,7 +1947,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -1961,7 +1961,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.begin.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1975,7 +1975,7 @@ }, { "c": "#eee", - "t": "string", + "t": "string.quoted.single.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -1989,7 +1989,7 @@ }, { "c": "'", - "t": "string", + "t": "string.quoted.single.ts punctuation.definition.string.end.ts", "r": { "dark_plus": "string: #CE9178", "light_plus": "string: #A31515", @@ -2003,7 +2003,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2017,7 +2017,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -2031,7 +2031,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2045,7 +2045,7 @@ }, { "c": "initialLifeProbability", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2059,7 +2059,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2073,7 +2073,7 @@ }, { "c": "0.5", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -2087,7 +2087,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2101,7 +2101,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -2115,7 +2115,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2129,7 +2129,7 @@ }, { "c": "animationRate", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2143,7 +2143,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2157,7 +2157,7 @@ }, { "c": "60", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -2171,7 +2171,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2185,7 +2185,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -2199,7 +2199,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2213,7 +2213,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2227,7 +2227,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2241,7 +2241,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -2255,7 +2255,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2269,7 +2269,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -2283,7 +2283,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2297,7 +2297,7 @@ }, { "c": "world", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2311,7 +2311,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2325,7 +2325,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -2339,7 +2339,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2353,7 +2353,7 @@ }, { "c": "createWorld", - "t": "entity.name.function", + "t": "variable.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -2367,7 +2367,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2381,7 +2381,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2395,7 +2395,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2409,7 +2409,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -2423,7 +2423,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2437,7 +2437,7 @@ }, { "c": "circleOfLife", - "t": "entity.name.function", + "t": "variable.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -2451,7 +2451,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2465,7 +2465,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2479,7 +2479,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2493,7 +2493,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2507,7 +2507,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -2521,7 +2521,7 @@ }, { "c": "createWorld", - "t": "entity.name.function", + "t": "variable.ts meta.definition.method.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -2535,7 +2535,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2549,7 +2549,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2563,7 +2563,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2577,7 +2577,7 @@ }, { "c": "return", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -2591,7 +2591,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -2605,7 +2605,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2619,7 +2619,7 @@ }, { "c": "travelWorld", - "t": "entity.name.function", + "t": "variable.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -2633,7 +2633,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2647,7 +2647,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2661,7 +2661,7 @@ }, { "c": "cell", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2675,7 +2675,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2689,7 +2689,7 @@ }, { "c": "Cell", - "t": "entity.name.type", + "t": "entity.name.type.ts meta.type.annotation.ts entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -2703,7 +2703,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2717,7 +2717,7 @@ }, { "c": "=>", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -2731,7 +2731,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2745,7 +2745,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2759,7 +2759,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2773,7 +2773,7 @@ }, { "c": "live", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2787,7 +2787,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2801,7 +2801,7 @@ }, { "c": "Math", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2815,7 +2815,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2829,7 +2829,7 @@ }, { "c": "random", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -2843,7 +2843,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2857,7 +2857,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2871,7 +2871,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -2885,7 +2885,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -2899,7 +2899,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2913,7 +2913,7 @@ }, { "c": "initialLifeProbability", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2927,7 +2927,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2941,7 +2941,7 @@ }, { "c": "return", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -2955,7 +2955,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -2969,7 +2969,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2983,7 +2983,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -2997,7 +2997,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3011,7 +3011,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3025,7 +3025,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3039,7 +3039,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -3053,7 +3053,7 @@ }, { "c": "circleOfLife", - "t": "entity.name.function", + "t": "variable.ts meta.definition.method.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -3067,7 +3067,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3081,7 +3081,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3095,7 +3095,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -3109,7 +3109,7 @@ }, { "c": "void", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -3123,7 +3123,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3137,7 +3137,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -3151,7 +3151,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3165,7 +3165,7 @@ }, { "c": "world", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -3179,7 +3179,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -3193,7 +3193,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -3207,7 +3207,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3221,7 +3221,7 @@ }, { "c": "travelWorld", - "t": "entity.name.function", + "t": "variable.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -3235,7 +3235,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3249,7 +3249,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3263,7 +3263,7 @@ }, { "c": "cell", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -3277,7 +3277,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -3291,7 +3291,7 @@ }, { "c": "Cell", - "t": "entity.name.type", + "t": "entity.name.type.ts meta.type.annotation.ts entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -3305,7 +3305,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3319,7 +3319,7 @@ }, { "c": "=>", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -3333,7 +3333,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3347,7 +3347,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -3361,7 +3361,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -3375,7 +3375,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -3389,7 +3389,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3403,7 +3403,7 @@ }, { "c": "world", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -3417,7 +3417,7 @@ }, { "c": "[", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3431,7 +3431,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -3445,7 +3445,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3459,7 +3459,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -3473,7 +3473,7 @@ }, { "c": "]", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3487,7 +3487,7 @@ }, { "c": "[", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3501,7 +3501,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -3515,7 +3515,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3529,7 +3529,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -3543,7 +3543,7 @@ }, { "c": "]", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3557,7 +3557,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3571,7 +3571,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -3585,7 +3585,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3599,7 +3599,7 @@ }, { "c": "draw", - "t": "entity.name.function", + "t": "variable.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -3613,7 +3613,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3627,7 +3627,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -3641,7 +3641,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3655,7 +3655,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3669,7 +3669,7 @@ }, { "c": "return", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -3683,7 +3683,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -3697,7 +3697,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3711,7 +3711,7 @@ }, { "c": "resolveNextGeneration", - "t": "entity.name.function", + "t": "variable.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -3725,7 +3725,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3739,7 +3739,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -3753,7 +3753,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3767,7 +3767,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3781,7 +3781,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3795,7 +3795,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3809,7 +3809,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3823,7 +3823,7 @@ }, { "c": "setTimeout", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -3837,7 +3837,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3851,7 +3851,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3865,7 +3865,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3879,7 +3879,7 @@ }, { "c": "=>", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -3893,7 +3893,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3907,7 +3907,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -3921,7 +3921,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3935,7 +3935,7 @@ }, { "c": "circleOfLife", - "t": "entity.name.function", + "t": "variable.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -3949,7 +3949,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3963,7 +3963,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3977,7 +3977,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -3991,7 +3991,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4005,7 +4005,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -4019,7 +4019,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4033,7 +4033,7 @@ }, { "c": "animationRate", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4047,7 +4047,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4061,7 +4061,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4075,7 +4075,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4089,7 +4089,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -4103,7 +4103,7 @@ }, { "c": "resolveNextGeneration", - "t": "entity.name.function", + "t": "variable.ts meta.definition.method.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -4117,7 +4117,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4131,7 +4131,7 @@ }, { "c": "cell", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4145,7 +4145,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -4159,7 +4159,7 @@ }, { "c": "Cell", - "t": "entity.name.type", + "t": "entity.name.type.ts meta.type.annotation.ts entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -4173,7 +4173,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4187,7 +4187,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4201,7 +4201,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -4215,7 +4215,7 @@ }, { "c": "count", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4229,7 +4229,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -4243,7 +4243,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -4257,7 +4257,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4271,7 +4271,7 @@ }, { "c": "countNeighbors", - "t": "entity.name.function", + "t": "variable.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -4285,7 +4285,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4299,7 +4299,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4313,7 +4313,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4327,7 +4327,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4341,7 +4341,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -4355,7 +4355,7 @@ }, { "c": "newCell", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4369,7 +4369,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -4383,7 +4383,7 @@ }, { "c": "new", - "t": "keyword.operator.new", + "t": "new.expr.ts keyword.operator.new.ts", "r": { "dark_plus": "keyword.operator.new: #569CD6", "light_plus": "keyword.operator.new: #0000FF", @@ -4397,7 +4397,7 @@ }, { "c": "Cell", - "t": "entity.name.function", + "t": "new.expr.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -4411,7 +4411,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4425,7 +4425,7 @@ }, { "c": "cell", - "t": "variable", + "t": "new.expr.ts variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4439,7 +4439,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4453,7 +4453,7 @@ }, { "c": "row", - "t": "variable", + "t": "new.expr.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4467,7 +4467,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4481,7 +4481,7 @@ }, { "c": "cell", - "t": "variable", + "t": "new.expr.ts variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4495,7 +4495,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4509,7 +4509,7 @@ }, { "c": "col", - "t": "variable", + "t": "new.expr.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4523,7 +4523,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4537,7 +4537,7 @@ }, { "c": "cell", - "t": "variable", + "t": "new.expr.ts variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4551,7 +4551,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4565,7 +4565,7 @@ }, { "c": "live", - "t": "variable", + "t": "new.expr.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4579,7 +4579,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4593,7 +4593,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4607,7 +4607,7 @@ }, { "c": "if", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -4621,7 +4621,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4635,7 +4635,7 @@ }, { "c": "count", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4649,7 +4649,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -4663,7 +4663,7 @@ }, { "c": "2", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -4677,7 +4677,7 @@ }, { "c": "||", - "t": "keyword.operator.logical", + "t": "keyword.operator.logical.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -4691,7 +4691,7 @@ }, { "c": "count", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4705,7 +4705,7 @@ }, { "c": ">", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -4719,7 +4719,7 @@ }, { "c": "3", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -4733,7 +4733,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4747,7 +4747,7 @@ }, { "c": "newCell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4761,7 +4761,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4775,7 +4775,7 @@ }, { "c": "live", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4789,7 +4789,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -4803,7 +4803,7 @@ }, { "c": "false", - "t": "constant.language.boolean.false", + "t": "constant.language.boolean.false.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -4817,7 +4817,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4831,7 +4831,7 @@ }, { "c": "else", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -4845,7 +4845,7 @@ }, { "c": "if", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -4859,7 +4859,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4873,7 +4873,7 @@ }, { "c": "count", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4887,7 +4887,7 @@ }, { "c": "==", - "t": "keyword.operator", + "t": "keyword.operator.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -4901,7 +4901,7 @@ }, { "c": "3", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -4915,7 +4915,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4929,7 +4929,7 @@ }, { "c": "newCell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4943,7 +4943,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4957,7 +4957,7 @@ }, { "c": "live", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -4971,7 +4971,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -4985,7 +4985,7 @@ }, { "c": "true", - "t": "constant.language.boolean.true", + "t": "constant.language.boolean.true.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -4999,7 +4999,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5013,7 +5013,7 @@ }, { "c": "return", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -5027,7 +5027,7 @@ }, { "c": "newCell", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5041,7 +5041,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5055,7 +5055,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5069,7 +5069,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -5083,7 +5083,7 @@ }, { "c": "countNeighbors", - "t": "entity.name.function", + "t": "variable.ts meta.definition.method.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -5097,7 +5097,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5111,7 +5111,7 @@ }, { "c": "cell", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5125,7 +5125,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -5139,7 +5139,7 @@ }, { "c": "Cell", - "t": "entity.name.type", + "t": "entity.name.type.ts meta.type.annotation.ts entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -5153,7 +5153,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5167,7 +5167,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5181,7 +5181,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -5195,7 +5195,7 @@ }, { "c": "neighbors", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5209,7 +5209,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -5223,7 +5223,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5237,7 +5237,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5251,7 +5251,7 @@ }, { "c": "for", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -5265,7 +5265,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5279,7 +5279,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -5293,7 +5293,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5307,7 +5307,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -5321,21 +5321,21 @@ }, { "c": "-", - "t": "", + "t": "keyword.operator.arithmetic.ts", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" } }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5349,7 +5349,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5363,7 +5363,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5377,7 +5377,7 @@ }, { "c": "<=", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -5391,7 +5391,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5405,7 +5405,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5419,7 +5419,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5433,7 +5433,7 @@ }, { "c": "++", - "t": "keyword.operator.increment", + "t": "keyword.operator.increment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -5447,7 +5447,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5461,7 +5461,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5475,7 +5475,7 @@ }, { "c": "for", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -5489,7 +5489,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5503,7 +5503,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -5517,7 +5517,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5531,7 +5531,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -5545,21 +5545,21 @@ }, { "c": "-", - "t": "", + "t": "keyword.operator.arithmetic.ts", "r": { - "dark_plus": "default: #D4D4D4", - "light_plus": "default: #000000", - "dark_vs": "default: #D4D4D4", - "light_vs": "default: #000000", - "hc_black": "default: #FFFFFF", - "dark_modern": "default: #CCCCCC", - "hc_light": "default: #292929", - "light_modern": "default: #3B3B3B" + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "dark_modern": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000", + "light_modern": "keyword.operator: #000000" } }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5573,7 +5573,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5587,7 +5587,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5601,7 +5601,7 @@ }, { "c": "<=", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -5615,7 +5615,7 @@ }, { "c": "1", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5629,7 +5629,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5643,7 +5643,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5657,7 +5657,7 @@ }, { "c": "++", - "t": "keyword.operator.increment", + "t": "keyword.operator.increment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -5671,7 +5671,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5685,7 +5685,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5699,7 +5699,7 @@ }, { "c": "if", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -5713,7 +5713,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5727,7 +5727,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5741,7 +5741,7 @@ }, { "c": "==", - "t": "keyword.operator", + "t": "keyword.operator.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -5755,7 +5755,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5769,7 +5769,7 @@ }, { "c": "&&", - "t": "keyword.operator.logical", + "t": "keyword.operator.logical.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -5783,7 +5783,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5797,7 +5797,7 @@ }, { "c": "==", - "t": "keyword.operator", + "t": "keyword.operator.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -5811,7 +5811,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -5825,7 +5825,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5839,7 +5839,7 @@ }, { "c": "continue", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -5853,7 +5853,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5867,7 +5867,7 @@ }, { "c": "if", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -5881,7 +5881,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5895,7 +5895,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -5909,7 +5909,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5923,7 +5923,7 @@ }, { "c": "isAlive", - "t": "entity.name.function", + "t": "variable.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -5937,7 +5937,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5951,7 +5951,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5965,7 +5965,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5979,7 +5979,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -5993,7 +5993,7 @@ }, { "c": "+", - "t": "keyword.operator.arithmetic", + "t": "keyword.operator.arithmetic.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6007,7 +6007,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6021,7 +6021,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6035,7 +6035,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6049,7 +6049,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6063,7 +6063,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6077,7 +6077,7 @@ }, { "c": "+", - "t": "keyword.operator.arithmetic", + "t": "keyword.operator.arithmetic.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6091,7 +6091,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6105,7 +6105,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6119,7 +6119,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6133,7 +6133,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6147,7 +6147,7 @@ }, { "c": "neighbors", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6161,7 +6161,7 @@ }, { "c": "++", - "t": "keyword.operator.increment", + "t": "keyword.operator.increment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6175,7 +6175,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6189,7 +6189,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6203,7 +6203,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6217,7 +6217,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6231,7 +6231,7 @@ }, { "c": "return", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -6245,7 +6245,7 @@ }, { "c": "neighbors", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6259,7 +6259,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6273,7 +6273,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6287,7 +6287,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -6301,7 +6301,7 @@ }, { "c": "isAlive", - "t": "entity.name.function", + "t": "variable.ts meta.definition.method.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -6315,7 +6315,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6329,7 +6329,7 @@ }, { "c": "row", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6343,7 +6343,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6357,7 +6357,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -6371,7 +6371,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6385,7 +6385,7 @@ }, { "c": "col", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6399,7 +6399,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6413,7 +6413,7 @@ }, { "c": "number", - "t": "support.type.primitive", + "t": "support.type.ts support.type.primitive.ts", "r": { "dark_plus": "support.type: #4EC9B0", "light_plus": "support.type: #267F99", @@ -6427,7 +6427,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6441,7 +6441,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6455,7 +6455,7 @@ }, { "c": "if", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -6469,7 +6469,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6483,7 +6483,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6497,7 +6497,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6511,7 +6511,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -6525,7 +6525,7 @@ }, { "c": "||", - "t": "keyword.operator.logical", + "t": "keyword.operator.logical.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6539,7 +6539,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6553,7 +6553,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6567,7 +6567,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -6581,7 +6581,7 @@ }, { "c": "||", - "t": "keyword.operator.logical", + "t": "keyword.operator.logical.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6595,7 +6595,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6609,7 +6609,7 @@ }, { "c": ">=", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6623,7 +6623,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -6637,7 +6637,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6651,7 +6651,7 @@ }, { "c": "gridSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6665,7 +6665,7 @@ }, { "c": "||", - "t": "keyword.operator.logical", + "t": "keyword.operator.logical.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6679,7 +6679,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6693,7 +6693,7 @@ }, { "c": ">=", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -6707,7 +6707,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -6721,7 +6721,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6735,7 +6735,7 @@ }, { "c": "gridSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6749,7 +6749,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6763,7 +6763,7 @@ }, { "c": "return", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -6777,7 +6777,7 @@ }, { "c": "false", - "t": "constant.language.boolean.false", + "t": "constant.language.boolean.false.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -6791,7 +6791,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6805,7 +6805,7 @@ }, { "c": "return", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -6819,7 +6819,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -6833,7 +6833,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6847,7 +6847,7 @@ }, { "c": "world", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6861,7 +6861,7 @@ }, { "c": "[", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6875,7 +6875,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6889,7 +6889,7 @@ }, { "c": "]", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6903,7 +6903,7 @@ }, { "c": "[", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6917,7 +6917,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6931,7 +6931,7 @@ }, { "c": "]", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6945,7 +6945,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6959,7 +6959,7 @@ }, { "c": "live", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -6973,7 +6973,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6987,7 +6987,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7001,7 +7001,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -7015,7 +7015,7 @@ }, { "c": "travelWorld", - "t": "entity.name.function", + "t": "variable.ts meta.definition.method.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -7029,7 +7029,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7043,7 +7043,7 @@ }, { "c": "callback", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7057,7 +7057,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7071,7 +7071,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7085,7 +7085,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -7099,7 +7099,7 @@ }, { "c": "result", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7113,7 +7113,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -7127,7 +7127,7 @@ }, { "c": "[", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7141,7 +7141,7 @@ }, { "c": "]", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7155,7 +7155,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7169,7 +7169,7 @@ }, { "c": "for", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -7183,7 +7183,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7197,7 +7197,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -7211,7 +7211,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7225,7 +7225,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -7239,7 +7239,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -7253,7 +7253,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7267,7 +7267,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7281,7 +7281,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -7295,7 +7295,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -7309,7 +7309,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7323,7 +7323,7 @@ }, { "c": "gridSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7337,7 +7337,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7351,7 +7351,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7365,7 +7365,7 @@ }, { "c": "++", - "t": "keyword.operator.increment", + "t": "keyword.operator.increment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -7379,7 +7379,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7393,7 +7393,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7407,7 +7407,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -7421,7 +7421,7 @@ }, { "c": "rowData", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7435,7 +7435,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -7449,7 +7449,7 @@ }, { "c": "[", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7463,7 +7463,7 @@ }, { "c": "]", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7477,7 +7477,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7491,7 +7491,7 @@ }, { "c": "for", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -7505,7 +7505,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7519,7 +7519,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -7533,7 +7533,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7547,7 +7547,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -7561,7 +7561,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -7575,7 +7575,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7589,7 +7589,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7603,7 +7603,7 @@ }, { "c": "<", - "t": "keyword.operator.relational", + "t": "keyword.operator.relational.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -7617,7 +7617,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -7631,7 +7631,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7645,7 +7645,7 @@ }, { "c": "gridSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7659,7 +7659,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7673,7 +7673,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7687,7 +7687,7 @@ }, { "c": "++", - "t": "keyword.operator.increment", + "t": "keyword.operator.increment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -7701,7 +7701,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7715,7 +7715,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7729,7 +7729,7 @@ }, { "c": "rowData", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7743,7 +7743,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7757,7 +7757,7 @@ }, { "c": "push", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -7771,7 +7771,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7785,7 +7785,7 @@ }, { "c": "callback", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -7799,7 +7799,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7813,7 +7813,7 @@ }, { "c": "new", - "t": "keyword.operator.new", + "t": "new.expr.ts keyword.operator.new.ts", "r": { "dark_plus": "keyword.operator.new: #569CD6", "light_plus": "keyword.operator.new: #0000FF", @@ -7827,7 +7827,7 @@ }, { "c": "Cell", - "t": "entity.name.function", + "t": "new.expr.ts variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -7841,7 +7841,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7855,7 +7855,7 @@ }, { "c": "row", - "t": "variable", + "t": "new.expr.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7869,7 +7869,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7883,7 +7883,7 @@ }, { "c": "col", - "t": "variable", + "t": "new.expr.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -7897,7 +7897,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7911,7 +7911,7 @@ }, { "c": "false", - "t": "constant.language.boolean.false", + "t": "new.expr.ts constant.language.boolean.false.ts", "r": { "dark_plus": "constant.language: #569CD6", "light_plus": "constant.language: #0000FF", @@ -7925,7 +7925,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7939,7 +7939,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7953,7 +7953,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7967,7 +7967,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7981,7 +7981,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -7995,7 +7995,7 @@ }, { "c": "result", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8009,7 +8009,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8023,7 +8023,7 @@ }, { "c": "push", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -8037,7 +8037,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8051,7 +8051,7 @@ }, { "c": "rowData", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8065,7 +8065,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8079,7 +8079,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8093,7 +8093,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8107,7 +8107,7 @@ }, { "c": "return", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -8121,7 +8121,7 @@ }, { "c": "result", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8135,7 +8135,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8149,7 +8149,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8163,7 +8163,7 @@ }, { "c": "public", - "t": "storage.modifier", + "t": "storage.modifier.ts", "r": { "dark_plus": "storage.modifier: #569CD6", "light_plus": "storage.modifier: #0000FF", @@ -8177,7 +8177,7 @@ }, { "c": "draw", - "t": "entity.name.function", + "t": "variable.ts meta.definition.method.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -8191,7 +8191,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8205,7 +8205,7 @@ }, { "c": "cell", - "t": "variable.parameter", + "t": "variable.ts variable.parameter.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8219,7 +8219,7 @@ }, { "c": ":", - "t": "keyword.operator.type.annotation", + "t": "punctuation.delimiter.ts keyword.operator.type.annotation.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -8233,7 +8233,7 @@ }, { "c": "Cell", - "t": "entity.name.type", + "t": "entity.name.type.ts meta.type.annotation.ts entity.name.type.ts", "r": { "dark_plus": "entity.name.type: #4EC9B0", "light_plus": "entity.name.type: #267F99", @@ -8247,7 +8247,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8261,7 +8261,7 @@ }, { "c": "{", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8275,7 +8275,7 @@ }, { "c": "if", - "t": "keyword.control", + "t": "keyword.control.ts", "r": { "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", @@ -8289,7 +8289,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8303,7 +8303,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -8317,7 +8317,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8331,7 +8331,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8345,7 +8345,7 @@ }, { "c": "==", - "t": "keyword.operator", + "t": "keyword.operator.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -8359,7 +8359,7 @@ }, { "c": "0", - "t": "constant.numeric", + "t": "constant.numeric.ts", "r": { "dark_plus": "constant.numeric: #B5CEA8", "light_plus": "constant.numeric: #098658", @@ -8373,7 +8373,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8387,7 +8387,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -8401,7 +8401,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8415,7 +8415,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8429,7 +8429,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -8443,7 +8443,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -8457,7 +8457,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8471,7 +8471,7 @@ }, { "c": "canvasSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8485,7 +8485,7 @@ }, { "c": "/", - "t": "keyword.operator.arithmetic", + "t": "keyword.operator.arithmetic.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -8499,7 +8499,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -8513,7 +8513,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8527,7 +8527,7 @@ }, { "c": "gridSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8541,7 +8541,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8555,7 +8555,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -8569,7 +8569,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8583,7 +8583,7 @@ }, { "c": "context", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8597,7 +8597,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8611,7 +8611,7 @@ }, { "c": "strokeStyle", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8625,7 +8625,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -8639,7 +8639,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -8653,7 +8653,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8667,7 +8667,7 @@ }, { "c": "lineColor", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8681,7 +8681,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8695,7 +8695,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -8709,7 +8709,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8723,7 +8723,7 @@ }, { "c": "context", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8737,7 +8737,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8751,7 +8751,7 @@ }, { "c": "strokeRect", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -8765,7 +8765,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8779,7 +8779,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8793,7 +8793,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8807,7 +8807,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8821,7 +8821,7 @@ }, { "c": "*", - "t": "keyword.operator.arithmetic", + "t": "keyword.operator.arithmetic.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -8835,7 +8835,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -8849,7 +8849,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8863,7 +8863,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8877,7 +8877,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8891,7 +8891,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8905,7 +8905,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8919,7 +8919,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8933,7 +8933,7 @@ }, { "c": "*", - "t": "keyword.operator.arithmetic", + "t": "keyword.operator.arithmetic.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -8947,7 +8947,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -8961,7 +8961,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -8975,7 +8975,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -8989,7 +8989,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9003,7 +9003,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -9017,7 +9017,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9031,7 +9031,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9045,7 +9045,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9059,7 +9059,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -9073,7 +9073,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9087,7 +9087,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9101,7 +9101,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9115,7 +9115,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9129,7 +9129,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -9143,7 +9143,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9157,7 +9157,7 @@ }, { "c": "context", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9171,7 +9171,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9185,7 +9185,7 @@ }, { "c": "fillStyle", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9199,7 +9199,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -9213,7 +9213,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9227,7 +9227,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9241,7 +9241,7 @@ }, { "c": "live", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9255,7 +9255,7 @@ }, { "c": "?", - "t": "keyword.operator.ternary", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -9269,7 +9269,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -9283,7 +9283,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9297,7 +9297,7 @@ }, { "c": "liveColor", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9311,7 +9311,7 @@ }, { "c": ":", - "t": "keyword.operator.ternary", + "t": "punctuation.delimiter.ts keyword.operator.ternary.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -9325,7 +9325,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -9339,7 +9339,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9353,7 +9353,7 @@ }, { "c": "deadColor", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9367,7 +9367,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9381,7 +9381,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -9395,7 +9395,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9409,7 +9409,7 @@ }, { "c": "context", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9423,7 +9423,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9437,7 +9437,7 @@ }, { "c": "fillRect", - "t": "entity.name.function", + "t": "variable.ts entity.name.function.ts", "r": { "dark_plus": "entity.name.function: #DCDCAA", "light_plus": "entity.name.function: #795E26", @@ -9451,7 +9451,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9465,7 +9465,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9479,7 +9479,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9493,7 +9493,7 @@ }, { "c": "row", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9507,7 +9507,7 @@ }, { "c": "*", - "t": "keyword.operator.arithmetic", + "t": "keyword.operator.arithmetic.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -9521,7 +9521,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -9535,7 +9535,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9549,7 +9549,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9563,7 +9563,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9577,7 +9577,7 @@ }, { "c": "cell", - "t": "variable", + "t": "variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9591,7 +9591,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9605,7 +9605,7 @@ }, { "c": "col", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9619,7 +9619,7 @@ }, { "c": "*", - "t": "keyword.operator.arithmetic", + "t": "keyword.operator.arithmetic.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -9633,7 +9633,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -9647,7 +9647,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9661,7 +9661,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9675,7 +9675,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9689,7 +9689,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -9703,7 +9703,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9717,7 +9717,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9731,7 +9731,7 @@ }, { "c": ",", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9745,7 +9745,7 @@ }, { "c": "this", - "t": "variable.language.this", + "t": "variable.language.this.ts", "r": { "dark_plus": "variable.language: #569CD6", "light_plus": "variable.language: #0000FF", @@ -9759,7 +9759,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9773,7 +9773,7 @@ }, { "c": "cellSize", - "t": "variable", + "t": "variable.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9787,7 +9787,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9801,7 +9801,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9815,7 +9815,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9829,7 +9829,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9843,7 +9843,7 @@ }, { "c": "}", - "t": "punctuation", + "t": "punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9857,7 +9857,7 @@ }, { "c": "var", - "t": "storage.type", + "t": "storage.type.ts", "r": { "dark_plus": "storage.type: #569CD6", "light_plus": "storage.type: #0000FF", @@ -9871,7 +9871,7 @@ }, { "c": "game", - "t": "variable", + "t": "variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9885,7 +9885,7 @@ }, { "c": "=", - "t": "keyword.operator.assignment", + "t": "keyword.operator.assignment.ts", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", @@ -9899,7 +9899,7 @@ }, { "c": "new", - "t": "keyword.operator.new", + "t": "new.expr.ts keyword.operator.new.ts", "r": { "dark_plus": "keyword.operator.new: #569CD6", "light_plus": "keyword.operator.new: #0000FF", @@ -9913,7 +9913,7 @@ }, { "c": "Conway", - "t": "variable", + "t": "new.expr.ts variable.ts variable.other.object.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9927,7 +9927,7 @@ }, { "c": ".", - "t": "punctuation.delimiter", + "t": "new.expr.ts punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9941,7 +9941,7 @@ }, { "c": "GameOfLife", - "t": "variable", + "t": "new.expr.ts variable.ts", "r": { "dark_plus": "variable: #9CDCFE", "light_plus": "variable: #001080", @@ -9955,7 +9955,7 @@ }, { "c": "(", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9969,7 +9969,7 @@ }, { "c": ")", - "t": "punctuation", + "t": "new.expr.ts punctuation.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -9983,7 +9983,7 @@ }, { "c": ";", - "t": "punctuation.delimiter", + "t": "punctuation.delimiter.ts", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", diff --git a/package-lock.json b/package-lock.json index d4bc1fb28bf..cdc7f3d3e12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,25 @@ { "name": "code-oss-dev", - "version": "1.98.0", + "version": "1.99.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-oss-dev", - "version": "1.98.0", + "version": "1.99.0", "hasInstallScript": true, "license": "MIT", "dependencies": { + "@c4312/eventsource-umd": "^3.0.5", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/policy-watcher": "^1.1.10", + "@vscode/policy-watcher": "^1.3.0", "@vscode/proxy-agent": "^0.32.0", - "@vscode/ripgrep": "^1.15.10", + "@vscode/ripgrep": "^1.15.11", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", "@vscode/sudo-prompt": "9.3.1", @@ -27,16 +28,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.81", - "@xterm/addon-image": "^0.9.0-beta.98", - "@xterm/addon-ligatures": "^0.10.0-beta.98", - "@xterm/addon-progress": "^0.2.0-beta.4", - "@xterm/addon-search": "^0.16.0-beta.98", - "@xterm/addon-serialize": "^0.14.0-beta.98", - "@xterm/addon-unicode11": "^0.9.0-beta.98", - "@xterm/addon-webgl": "^0.19.0-beta.98", - "@xterm/headless": "^5.6.0-beta.98", - "@xterm/xterm": "^5.6.0-beta.98", + "@xterm/addon-clipboard": "^0.2.0-beta.82", + "@xterm/addon-image": "^0.9.0-beta.99", + "@xterm/addon-ligatures": "^0.10.0-beta.99", + "@xterm/addon-progress": "^0.2.0-beta.5", + "@xterm/addon-search": "^0.16.0-beta.99", + "@xterm/addon-serialize": "^0.14.0-beta.99", + "@xterm/addon-unicode11": "^0.9.0-beta.99", + "@xterm/addon-webgl": "^0.19.0-beta.99", + "@xterm/headless": "^5.6.0-beta.99", + "@xterm/xterm": "^5.6.0-beta.99", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -45,7 +46,7 @@ "native-is-elevated": "0.7.0", "native-keymap": "^3.3.5", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta30", + "node-pty": "1.1.0-beta31", "open": "^8.4.2", "tas-client-umd": "0.2.0", "v8-inspect-profiler": "^0.1.1", @@ -96,7 +97,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "34.2.0", + "electron": "34.3.2", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", @@ -941,6 +942,18 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@c4312/eventsource-umd": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@c4312/eventsource-umd/-/eventsource-umd-3.0.5.tgz", + "integrity": "sha512-0QhLg51eFB+SS/a4Pv5tHaRSnjJBpdFsjT3WN/Vfh6qzeFXqvaE+evVIIToYvr2lRBLg1NIB635ip8ML+/84Sg==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2830,9 +2843,9 @@ } }, "node_modules/@vscode/policy-watcher": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@vscode/policy-watcher/-/policy-watcher-1.1.10.tgz", - "integrity": "sha512-erRJiryjhP//MnRZo+j0LxjVSFa4eZMR9HeAGxIuxlZCQrnvrIG5nv/4qBxiMH0+uE4Z74YY/Ct0wus6l9U/xg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@vscode/policy-watcher/-/policy-watcher-1.3.0.tgz", + "integrity": "sha512-a8pPxlZlMJWOOj2NZ/2ceXgHdDU/NXo+8Pn/InV/sPBfbvTnf/MpMc4pscm9pdU4UIrTGR5+OduQW7mTK8DK7Q==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2868,9 +2881,9 @@ } }, "node_modules/@vscode/ripgrep": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.10.tgz", - "integrity": "sha512-83Q6qFrELpFgf88bPOcwSWDegfY2r/cb6bIfdLTSZvN73Dg1wviSfO+1v6lTFMd0mAvUYYcTUu+Mn5xMroZMxA==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.11.tgz", + "integrity": "sha512-G/VqtA6kR50mJkIH4WA+I2Q78V5blovgPPq0VPYM0QIRp57lYMkdV+U9VrY80b3AvaC72A1z8STmyxc8ZKiTsw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -3442,30 +3455,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.81", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.81.tgz", - "integrity": "sha512-vDxRyBO9VHzsl+gUQsDlUM9o2ZxSJKzE2eYQtuILKcf5D0EXYI86aMwKT/1iguX41vcMg42WXQBQ0TLWZ2MyTQ==", + "version": "0.2.0-beta.82", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.82.tgz", + "integrity": "sha512-6PCRV0AHm/+ogeRdz2Txndau3l2Z7X7Buu8v5kpnNB30DKyvMh5p9J35maBPIwKF8XUSBvgywu+AW5x6mVqu9g==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.98.tgz", - "integrity": "sha512-yJaezwUc1Y3QYCmYSpjFW9IzMTLPSqrRCgdPnQ0JbCAVASRVmH6DLRfeikheJCvoXW6VUwMmGkb3fSplTxiV1w==", + "version": "0.9.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.99.tgz", + "integrity": "sha512-fU6VsnB3X6RUVo5Y2ZACEnbS/3CSFPhWxkDML6r+fgPz6pV4IwGBFLuyvUPxfyfpYt5+3muh6ChDDwUjxG1Ldg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.98.tgz", - "integrity": "sha512-1zYeS9OUBR2ThG7dsxsGKOqeSlUo+DNTd5aaV5ZFbKQsQ1w+sQCaL73ZrXp1kuVK7pBiP5NCo4ffcXfzcztztA==", + "version": "0.10.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.99.tgz", + "integrity": "sha512-QlhUtBlIC7ZgEykpWxFl5lc2MtIFJD41pT8bQVRD1wGShgUmceNTk4xd3CjiQdVOtTrHcgOTM75YmS5GOlobOA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -3475,64 +3488,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.4", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.4.tgz", - "integrity": "sha512-7t1nlaANdEjUBVvuTTs5gw6UQgB+unFLwSGGnYXIvdQroYdkXQXSSATSqpYKVCd/6QFhBerzdB2VwPM5L5lxIw==", + "version": "0.2.0-beta.5", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.5.tgz", + "integrity": "sha512-6dfUtCqK/anFiVilv1KNyVWbEql2hJwINlAXnl5YtIyEwR8F/i+zWBuzUj9152gT3rDASTmgXE5HG6mnyaUI9w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.98.tgz", - "integrity": "sha512-7gtx7eYvwFLxlb5q2IKxa7jG1KEinVwTlT3ijnSsPawwn7rGi6nR135rGiR8DAjEV0GUO406ICeoYyVbgiFNwQ==", + "version": "0.16.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.99.tgz", + "integrity": "sha512-nyqGsZDR/l0Gp4gaS+Brrjm53dpaNAqOUtAC8BXmvuzK21sQgyLsC99MTLNR5yh3dYJAfWAAhG5ke/Re3AaamQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.98.tgz", - "integrity": "sha512-Tsr8j3wnun2raYR1DgsNClQP/I5a85u/uW/5EiYH+/iPPua6EWJvPlr5Q6TCU/cdIKW1o27Z3L5/mw0pfMUXrQ==", + "version": "0.14.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.99.tgz", + "integrity": "sha512-7ULW4BUUGL1Bv8vGBNylaBTpKFDm7rjMdkOwJt+LVd/oJkyL8RFSGgQSuYb+5DyiEhBSpeahg3bi8bStxufvMQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.98.tgz", - "integrity": "sha512-3GjjEYAPWAEGE1CaTFDjQxKcY5NiHHPmeufTsRp3IL5850ZiaMMq9bIL2WSdoFIbVgfbxh5mRy2cJPdu9m0uRQ==", + "version": "0.9.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.99.tgz", + "integrity": "sha512-4epGzbOc7X+NyPIMPxnQxaUlZYhCRTEPRsvfuIx55+Yyzip/zGX5ahy/Z22YrGTVv7qjxhVsu1tCbCgiF9HtTA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.98.tgz", - "integrity": "sha512-09FbNHgN2ad/8JI+AEyg8C3msyk04ET1FihQIpTeWPfd2LJIAdps7G4St2+qzZbhlFkR6m9Dgrgh/AC2uegh8A==", + "version": "0.19.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.99.tgz", + "integrity": "sha512-t4vTtwDLYWgzcH86s3hlCGaZWJWzTXLXUcgw/2l+Fkq9LFy4cLuQgWTVjOWLB0KOJ0FmT+g0sBWLApUw9bYa2w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.98.tgz", - "integrity": "sha512-nRl5NyNHajvxNy1N/h0+vEO9tgBgYU8kN/SApBzezGYjcKhs81MYVr5uO1uMKb2eq5eTXBW8BtcyD3KqGoqkEA==", + "version": "5.6.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.99.tgz", + "integrity": "sha512-E+TR7Cgdb9tHGYw96cexH9l5ghsQEFfw4LaXKxmdlogs43qk2HPwwI7fR/i7t7Ci9ScBXf2gMP76NPpfeX1hZQ==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.98.tgz", - "integrity": "sha512-fJexj3XKDAMGsR8KKaiEhGrtJD1eRANbT3094E3KSgvbHRa3524tSFvDCx5+5KRE/hYaECmi0knAUIWJCvSPTg==", + "version": "5.6.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.99.tgz", + "integrity": "sha512-TwBXSyio63Sr2+eJ24BtrPiwTA8JpRbdzhNBYzCXs32yWX30X47UAcdgkahjkyt4JHSqhu7614/w5FOzHsNc/g==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { @@ -6099,9 +6112,9 @@ "dev": true }, "node_modules/electron": { - "version": "34.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-34.2.0.tgz", - "integrity": "sha512-SYwBJNeXBTm1q/ErybQMUBZAYqEreBUqBwTrNkw1rV4YatDZk5Aittpcus3PPeC4UoI/tqmJ946uG8AKHTd6CA==", + "version": "34.3.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-34.3.2.tgz", + "integrity": "sha512-n9tzmFexVLxipZXwMTY30H10f0X9k2OP0SkpSwL5VvnDZi0l/Hc+8CEArKkQPbbSf/IS7nxgc96gtTaR+XoSBg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6687,6 +6700,15 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", + "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -12626,9 +12648,9 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta30", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta30.tgz", - "integrity": "sha512-cmNYVWfbf961aOqnxIFXssvw6Fp6/78BQBNlwYRWUHBenJjUhCJ1wMZpJy+SegoLC07P9D6HTtq39Kd89rpv/w==", + "version": "1.1.0-beta31", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta31.tgz", + "integrity": "sha512-DwNyk7nQ8NfHX7NrIqvNQ5GiK6eBbsRYJ+hvHK04PTzZ6o5j1Qsc67g0QxXW8tki/ZJmE9Zxw6PEGncvDshdVw==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 925462fb087..14097a90d9e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.98.0", - "distro": "aee970faa436e6fdd88d579c1409364c3fdab6a8", + "version": "1.99.0", + "distro": "c3ec5ba4852b5682b94358c92bf31484d2739db9", "author": { "name": "Microsoft Corporation" }, @@ -45,6 +45,7 @@ "tsec-compile-check": "node node_modules/tsec/bin/tsec -p src/tsconfig.tsec.json", "vscode-dts-compile-check": "tsc -p src/tsconfig.vscode-dts.json && tsc -p src/tsconfig.vscode-proposed-dts.json", "valid-layers-check": "node build/lib/layersChecker.js", + "property-init-order-check": "node build/lib/propertyInitOrderChecker.js", "update-distro": "node build/npm/update-distro.mjs", "web": "echo 'npm run web' is replaced by './scripts/code-server' or './scripts/code-web'", "compile-cli": "gulp compile-cli", @@ -68,15 +69,16 @@ "update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json" }, "dependencies": { + "@c4312/eventsource-umd": "^3.0.5", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/policy-watcher": "^1.1.10", + "@vscode/policy-watcher": "^1.3.0", "@vscode/proxy-agent": "^0.32.0", - "@vscode/ripgrep": "^1.15.10", + "@vscode/ripgrep": "^1.15.11", "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.8-vscode", "@vscode/sudo-prompt": "9.3.1", @@ -85,16 +87,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.81", - "@xterm/addon-image": "^0.9.0-beta.98", - "@xterm/addon-ligatures": "^0.10.0-beta.98", - "@xterm/addon-progress": "^0.2.0-beta.4", - "@xterm/addon-search": "^0.16.0-beta.98", - "@xterm/addon-serialize": "^0.14.0-beta.98", - "@xterm/addon-unicode11": "^0.9.0-beta.98", - "@xterm/addon-webgl": "^0.19.0-beta.98", - "@xterm/headless": "^5.6.0-beta.98", - "@xterm/xterm": "^5.6.0-beta.98", + "@xterm/addon-clipboard": "^0.2.0-beta.82", + "@xterm/addon-image": "^0.9.0-beta.99", + "@xterm/addon-ligatures": "^0.10.0-beta.99", + "@xterm/addon-progress": "^0.2.0-beta.5", + "@xterm/addon-search": "^0.16.0-beta.99", + "@xterm/addon-serialize": "^0.14.0-beta.99", + "@xterm/addon-unicode11": "^0.9.0-beta.99", + "@xterm/addon-webgl": "^0.19.0-beta.99", + "@xterm/headless": "^5.6.0-beta.99", + "@xterm/xterm": "^5.6.0-beta.99", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -103,7 +105,7 @@ "native-is-elevated": "0.7.0", "native-keymap": "^3.3.5", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta30", + "node-pty": "1.1.0-beta31", "open": "^8.4.2", "tas-client-umd": "0.2.0", "v8-inspect-profiler": "^0.1.1", @@ -154,7 +156,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "34.2.0", + "electron": "34.3.2", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", diff --git a/product.json b/product.json index 873ea0eaa13..1d4dca8fc3f 100644 --- a/product.json +++ b/product.json @@ -25,6 +25,8 @@ "win32TunnelServiceMutex": "vscodeoss-tunnelservice", "win32TunnelMutex": "vscodeoss-tunnel", "darwinBundleIdentifier": "com.visualstudio.code.oss", + "darwinProfileUUID": "47827DD9-4734-49A0-AF80-7E19B11495CC", + "darwinProfilePayloadUUID": "CF808BE7-53F3-46C6-A7E2-7EDB98A5E959", "linuxIconName": "code-oss", "licenseFileName": "LICENSE.txt", "reportIssueUrl": "https://github.com/microsoft/vscode/issues/new", diff --git a/remote/.npmrc b/remote/.npmrc index 1d8b1e87782..e2c53927b15 100644 --- a/remote/.npmrc +++ b/remote/.npmrc @@ -1,6 +1,6 @@ disturl="https://nodejs.org/dist" -target="20.18.2" -ms_build_id="320948" +target="20.18.3" +ms_build_id="323695" runtime="node" build_from_source="true" legacy-peer-deps="true" diff --git a/remote/package-lock.json b/remote/package-lock.json index 2349babd03e..e700567c779 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -14,22 +14,22 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/proxy-agent": "^0.32.0", - "@vscode/ripgrep": "^1.15.10", + "@vscode/ripgrep": "^1.15.11", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.1.3", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.81", - "@xterm/addon-image": "^0.9.0-beta.98", - "@xterm/addon-ligatures": "^0.10.0-beta.98", - "@xterm/addon-progress": "^0.2.0-beta.4", - "@xterm/addon-search": "^0.16.0-beta.98", - "@xterm/addon-serialize": "^0.14.0-beta.98", - "@xterm/addon-unicode11": "^0.9.0-beta.98", - "@xterm/addon-webgl": "^0.19.0-beta.98", - "@xterm/headless": "^5.6.0-beta.98", - "@xterm/xterm": "^5.6.0-beta.98", + "@xterm/addon-clipboard": "^0.2.0-beta.82", + "@xterm/addon-image": "^0.9.0-beta.99", + "@xterm/addon-ligatures": "^0.10.0-beta.99", + "@xterm/addon-progress": "^0.2.0-beta.5", + "@xterm/addon-search": "^0.16.0-beta.99", + "@xterm/addon-serialize": "^0.14.0-beta.99", + "@xterm/addon-unicode11": "^0.9.0-beta.99", + "@xterm/addon-webgl": "^0.19.0-beta.99", + "@xterm/headless": "^5.6.0-beta.99", + "@xterm/xterm": "^5.6.0-beta.99", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -37,7 +37,7 @@ "kerberos": "2.1.1", "minimist": "^1.2.6", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta30", + "node-pty": "1.1.0-beta31", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", @@ -437,9 +437,9 @@ } }, "node_modules/@vscode/ripgrep": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.10.tgz", - "integrity": "sha512-83Q6qFrELpFgf88bPOcwSWDegfY2r/cb6bIfdLTSZvN73Dg1wviSfO+1v6lTFMd0mAvUYYcTUu+Mn5xMroZMxA==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.11.tgz", + "integrity": "sha512-G/VqtA6kR50mJkIH4WA+I2Q78V5blovgPPq0VPYM0QIRp57lYMkdV+U9VrY80b3AvaC72A1z8STmyxc8ZKiTsw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -523,30 +523,30 @@ "hasInstallScript": true }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.81", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.81.tgz", - "integrity": "sha512-vDxRyBO9VHzsl+gUQsDlUM9o2ZxSJKzE2eYQtuILKcf5D0EXYI86aMwKT/1iguX41vcMg42WXQBQ0TLWZ2MyTQ==", + "version": "0.2.0-beta.82", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.82.tgz", + "integrity": "sha512-6PCRV0AHm/+ogeRdz2Txndau3l2Z7X7Buu8v5kpnNB30DKyvMh5p9J35maBPIwKF8XUSBvgywu+AW5x6mVqu9g==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.98.tgz", - "integrity": "sha512-yJaezwUc1Y3QYCmYSpjFW9IzMTLPSqrRCgdPnQ0JbCAVASRVmH6DLRfeikheJCvoXW6VUwMmGkb3fSplTxiV1w==", + "version": "0.9.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.99.tgz", + "integrity": "sha512-fU6VsnB3X6RUVo5Y2ZACEnbS/3CSFPhWxkDML6r+fgPz6pV4IwGBFLuyvUPxfyfpYt5+3muh6ChDDwUjxG1Ldg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.98.tgz", - "integrity": "sha512-1zYeS9OUBR2ThG7dsxsGKOqeSlUo+DNTd5aaV5ZFbKQsQ1w+sQCaL73ZrXp1kuVK7pBiP5NCo4ffcXfzcztztA==", + "version": "0.10.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.99.tgz", + "integrity": "sha512-QlhUtBlIC7ZgEykpWxFl5lc2MtIFJD41pT8bQVRD1wGShgUmceNTk4xd3CjiQdVOtTrHcgOTM75YmS5GOlobOA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -556,64 +556,64 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.4", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.4.tgz", - "integrity": "sha512-7t1nlaANdEjUBVvuTTs5gw6UQgB+unFLwSGGnYXIvdQroYdkXQXSSATSqpYKVCd/6QFhBerzdB2VwPM5L5lxIw==", + "version": "0.2.0-beta.5", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.5.tgz", + "integrity": "sha512-6dfUtCqK/anFiVilv1KNyVWbEql2hJwINlAXnl5YtIyEwR8F/i+zWBuzUj9152gT3rDASTmgXE5HG6mnyaUI9w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.98.tgz", - "integrity": "sha512-7gtx7eYvwFLxlb5q2IKxa7jG1KEinVwTlT3ijnSsPawwn7rGi6nR135rGiR8DAjEV0GUO406ICeoYyVbgiFNwQ==", + "version": "0.16.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.99.tgz", + "integrity": "sha512-nyqGsZDR/l0Gp4gaS+Brrjm53dpaNAqOUtAC8BXmvuzK21sQgyLsC99MTLNR5yh3dYJAfWAAhG5ke/Re3AaamQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.98.tgz", - "integrity": "sha512-Tsr8j3wnun2raYR1DgsNClQP/I5a85u/uW/5EiYH+/iPPua6EWJvPlr5Q6TCU/cdIKW1o27Z3L5/mw0pfMUXrQ==", + "version": "0.14.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.99.tgz", + "integrity": "sha512-7ULW4BUUGL1Bv8vGBNylaBTpKFDm7rjMdkOwJt+LVd/oJkyL8RFSGgQSuYb+5DyiEhBSpeahg3bi8bStxufvMQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.98.tgz", - "integrity": "sha512-3GjjEYAPWAEGE1CaTFDjQxKcY5NiHHPmeufTsRp3IL5850ZiaMMq9bIL2WSdoFIbVgfbxh5mRy2cJPdu9m0uRQ==", + "version": "0.9.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.99.tgz", + "integrity": "sha512-4epGzbOc7X+NyPIMPxnQxaUlZYhCRTEPRsvfuIx55+Yyzip/zGX5ahy/Z22YrGTVv7qjxhVsu1tCbCgiF9HtTA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.98.tgz", - "integrity": "sha512-09FbNHgN2ad/8JI+AEyg8C3msyk04ET1FihQIpTeWPfd2LJIAdps7G4St2+qzZbhlFkR6m9Dgrgh/AC2uegh8A==", + "version": "0.19.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.99.tgz", + "integrity": "sha512-t4vTtwDLYWgzcH86s3hlCGaZWJWzTXLXUcgw/2l+Fkq9LFy4cLuQgWTVjOWLB0KOJ0FmT+g0sBWLApUw9bYa2w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.98.tgz", - "integrity": "sha512-nRl5NyNHajvxNy1N/h0+vEO9tgBgYU8kN/SApBzezGYjcKhs81MYVr5uO1uMKb2eq5eTXBW8BtcyD3KqGoqkEA==", + "version": "5.6.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.99.tgz", + "integrity": "sha512-E+TR7Cgdb9tHGYw96cexH9l5ghsQEFfw4LaXKxmdlogs43qk2HPwwI7fR/i7t7Ci9ScBXf2gMP76NPpfeX1hZQ==", "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.98.tgz", - "integrity": "sha512-fJexj3XKDAMGsR8KKaiEhGrtJD1eRANbT3094E3KSgvbHRa3524tSFvDCx5+5KRE/hYaECmi0knAUIWJCvSPTg==", + "version": "5.6.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.99.tgz", + "integrity": "sha512-TwBXSyio63Sr2+eJ24BtrPiwTA8JpRbdzhNBYzCXs32yWX30X47UAcdgkahjkyt4JHSqhu7614/w5FOzHsNc/g==", "license": "MIT" }, "node_modules/agent-base": { @@ -1098,9 +1098,9 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta30", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta30.tgz", - "integrity": "sha512-cmNYVWfbf961aOqnxIFXssvw6Fp6/78BQBNlwYRWUHBenJjUhCJ1wMZpJy+SegoLC07P9D6HTtq39Kd89rpv/w==", + "version": "1.1.0-beta31", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta31.tgz", + "integrity": "sha512-DwNyk7nQ8NfHX7NrIqvNQ5GiK6eBbsRYJ+hvHK04PTzZ6o5j1Qsc67g0QxXW8tki/ZJmE9Zxw6PEGncvDshdVw==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/remote/package.json b/remote/package.json index a25b884dedb..1cdd6ec37af 100644 --- a/remote/package.json +++ b/remote/package.json @@ -9,22 +9,22 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/proxy-agent": "^0.32.0", - "@vscode/ripgrep": "^1.15.10", + "@vscode/ripgrep": "^1.15.11", "@vscode/spdlog": "^0.15.0", "@vscode/tree-sitter-wasm": "^0.1.3", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.81", - "@xterm/addon-image": "^0.9.0-beta.98", - "@xterm/addon-ligatures": "^0.10.0-beta.98", - "@xterm/addon-progress": "^0.2.0-beta.4", - "@xterm/addon-search": "^0.16.0-beta.98", - "@xterm/addon-serialize": "^0.14.0-beta.98", - "@xterm/addon-unicode11": "^0.9.0-beta.98", - "@xterm/addon-webgl": "^0.19.0-beta.98", - "@xterm/headless": "^5.6.0-beta.98", - "@xterm/xterm": "^5.6.0-beta.98", + "@xterm/addon-clipboard": "^0.2.0-beta.82", + "@xterm/addon-image": "^0.9.0-beta.99", + "@xterm/addon-ligatures": "^0.10.0-beta.99", + "@xterm/addon-progress": "^0.2.0-beta.5", + "@xterm/addon-search": "^0.16.0-beta.99", + "@xterm/addon-serialize": "^0.14.0-beta.99", + "@xterm/addon-unicode11": "^0.9.0-beta.99", + "@xterm/addon-webgl": "^0.19.0-beta.99", + "@xterm/headless": "^5.6.0-beta.99", + "@xterm/xterm": "^5.6.0-beta.99", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -32,7 +32,7 @@ "kerberos": "2.1.1", "minimist": "^1.2.6", "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta30", + "node-pty": "1.1.0-beta31", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index e13c2c65e52..235d2d3ba46 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -13,15 +13,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.1.3", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.81", - "@xterm/addon-image": "^0.9.0-beta.98", - "@xterm/addon-ligatures": "^0.10.0-beta.98", - "@xterm/addon-progress": "^0.2.0-beta.4", - "@xterm/addon-search": "^0.16.0-beta.98", - "@xterm/addon-serialize": "^0.14.0-beta.98", - "@xterm/addon-unicode11": "^0.9.0-beta.98", - "@xterm/addon-webgl": "^0.19.0-beta.98", - "@xterm/xterm": "^5.6.0-beta.98", + "@xterm/addon-clipboard": "^0.2.0-beta.82", + "@xterm/addon-image": "^0.9.0-beta.99", + "@xterm/addon-ligatures": "^0.10.0-beta.99", + "@xterm/addon-progress": "^0.2.0-beta.5", + "@xterm/addon-search": "^0.16.0-beta.99", + "@xterm/addon-serialize": "^0.14.0-beta.99", + "@xterm/addon-unicode11": "^0.9.0-beta.99", + "@xterm/addon-webgl": "^0.19.0-beta.99", + "@xterm/xterm": "^5.6.0-beta.99", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", @@ -90,30 +90,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.81", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.81.tgz", - "integrity": "sha512-vDxRyBO9VHzsl+gUQsDlUM9o2ZxSJKzE2eYQtuILKcf5D0EXYI86aMwKT/1iguX41vcMg42WXQBQ0TLWZ2MyTQ==", + "version": "0.2.0-beta.82", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.82.tgz", + "integrity": "sha512-6PCRV0AHm/+ogeRdz2Txndau3l2Z7X7Buu8v5kpnNB30DKyvMh5p9J35maBPIwKF8XUSBvgywu+AW5x6mVqu9g==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.98.tgz", - "integrity": "sha512-yJaezwUc1Y3QYCmYSpjFW9IzMTLPSqrRCgdPnQ0JbCAVASRVmH6DLRfeikheJCvoXW6VUwMmGkb3fSplTxiV1w==", + "version": "0.9.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.99.tgz", + "integrity": "sha512-fU6VsnB3X6RUVo5Y2ZACEnbS/3CSFPhWxkDML6r+fgPz6pV4IwGBFLuyvUPxfyfpYt5+3muh6ChDDwUjxG1Ldg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.98.tgz", - "integrity": "sha512-1zYeS9OUBR2ThG7dsxsGKOqeSlUo+DNTd5aaV5ZFbKQsQ1w+sQCaL73ZrXp1kuVK7pBiP5NCo4ffcXfzcztztA==", + "version": "0.10.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.99.tgz", + "integrity": "sha512-QlhUtBlIC7ZgEykpWxFl5lc2MtIFJD41pT8bQVRD1wGShgUmceNTk4xd3CjiQdVOtTrHcgOTM75YmS5GOlobOA==", "license": "MIT", "dependencies": { "font-finder": "^1.1.0", @@ -123,58 +123,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.4", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.4.tgz", - "integrity": "sha512-7t1nlaANdEjUBVvuTTs5gw6UQgB+unFLwSGGnYXIvdQroYdkXQXSSATSqpYKVCd/6QFhBerzdB2VwPM5L5lxIw==", + "version": "0.2.0-beta.5", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.5.tgz", + "integrity": "sha512-6dfUtCqK/anFiVilv1KNyVWbEql2hJwINlAXnl5YtIyEwR8F/i+zWBuzUj9152gT3rDASTmgXE5HG6mnyaUI9w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.98.tgz", - "integrity": "sha512-7gtx7eYvwFLxlb5q2IKxa7jG1KEinVwTlT3ijnSsPawwn7rGi6nR135rGiR8DAjEV0GUO406ICeoYyVbgiFNwQ==", + "version": "0.16.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.99.tgz", + "integrity": "sha512-nyqGsZDR/l0Gp4gaS+Brrjm53dpaNAqOUtAC8BXmvuzK21sQgyLsC99MTLNR5yh3dYJAfWAAhG5ke/Re3AaamQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.98.tgz", - "integrity": "sha512-Tsr8j3wnun2raYR1DgsNClQP/I5a85u/uW/5EiYH+/iPPua6EWJvPlr5Q6TCU/cdIKW1o27Z3L5/mw0pfMUXrQ==", + "version": "0.14.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.99.tgz", + "integrity": "sha512-7ULW4BUUGL1Bv8vGBNylaBTpKFDm7rjMdkOwJt+LVd/oJkyL8RFSGgQSuYb+5DyiEhBSpeahg3bi8bStxufvMQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.98.tgz", - "integrity": "sha512-3GjjEYAPWAEGE1CaTFDjQxKcY5NiHHPmeufTsRp3IL5850ZiaMMq9bIL2WSdoFIbVgfbxh5mRy2cJPdu9m0uRQ==", + "version": "0.9.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.99.tgz", + "integrity": "sha512-4epGzbOc7X+NyPIMPxnQxaUlZYhCRTEPRsvfuIx55+Yyzip/zGX5ahy/Z22YrGTVv7qjxhVsu1tCbCgiF9HtTA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.98.tgz", - "integrity": "sha512-09FbNHgN2ad/8JI+AEyg8C3msyk04ET1FihQIpTeWPfd2LJIAdps7G4St2+qzZbhlFkR6m9Dgrgh/AC2uegh8A==", + "version": "0.19.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.99.tgz", + "integrity": "sha512-t4vTtwDLYWgzcH86s3hlCGaZWJWzTXLXUcgw/2l+Fkq9LFy4cLuQgWTVjOWLB0KOJ0FmT+g0sBWLApUw9bYa2w==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.98" + "@xterm/xterm": "^5.6.0-beta.99" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.98", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.98.tgz", - "integrity": "sha512-fJexj3XKDAMGsR8KKaiEhGrtJD1eRANbT3094E3KSgvbHRa3524tSFvDCx5+5KRE/hYaECmi0knAUIWJCvSPTg==", + "version": "5.6.0-beta.99", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.99.tgz", + "integrity": "sha512-TwBXSyio63Sr2+eJ24BtrPiwTA8JpRbdzhNBYzCXs32yWX30X47UAcdgkahjkyt4JHSqhu7614/w5FOzHsNc/g==", "license": "MIT" }, "node_modules/font-finder": { diff --git a/remote/web/package.json b/remote/web/package.json index 78336aa2fa4..c54922cdb2e 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -8,15 +8,15 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/tree-sitter-wasm": "^0.1.3", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.81", - "@xterm/addon-image": "^0.9.0-beta.98", - "@xterm/addon-ligatures": "^0.10.0-beta.98", - "@xterm/addon-progress": "^0.2.0-beta.4", - "@xterm/addon-search": "^0.16.0-beta.98", - "@xterm/addon-serialize": "^0.14.0-beta.98", - "@xterm/addon-unicode11": "^0.9.0-beta.98", - "@xterm/addon-webgl": "^0.19.0-beta.98", - "@xterm/xterm": "^5.6.0-beta.98", + "@xterm/addon-clipboard": "^0.2.0-beta.82", + "@xterm/addon-image": "^0.9.0-beta.99", + "@xterm/addon-ligatures": "^0.10.0-beta.99", + "@xterm/addon-progress": "^0.2.0-beta.5", + "@xterm/addon-search": "^0.16.0-beta.99", + "@xterm/addon-serialize": "^0.14.0-beta.99", + "@xterm/addon-unicode11": "^0.9.0-beta.99", + "@xterm/addon-webgl": "^0.19.0-beta.99", + "@xterm/xterm": "^5.6.0-beta.99", "jschardet": "3.1.4", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", diff --git a/resources/server/bin/code-server-linux.sh b/resources/server/bin/code-server-linux.sh index 3df32dfd43c..20df7cda4df 100644 --- a/resources/server/bin/code-server-linux.sh +++ b/resources/server/bin/code-server-linux.sh @@ -9,4 +9,14 @@ esac ROOT="$(dirname "$(dirname "$(readlink -f "$0")")")" +# Set rpath before changing the interpreter path +# Refs https://github.com/NixOS/patchelf/issues/524 +if [ -n "$VSCODE_SERVER_CUSTOM_GLIBC_LINKER" ] && [ -n "$VSCODE_SERVER_CUSTOM_GLIBC_PATH" ] && [ -n "$VSCODE_SERVER_PATCHELF_PATH" ]; then + echo "Patching glibc from $VSCODE_SERVER_CUSTOM_GLIBC_PATH with $VSCODE_SERVER_PATCHELF_PATH..." + "$VSCODE_SERVER_PATCHELF_PATH" --set-rpath "$VSCODE_SERVER_CUSTOM_GLIBC_PATH" "$ROOT/node" + echo "Patching linker from $VSCODE_SERVER_CUSTOM_GLIBC_LINKER with $VSCODE_SERVER_PATCHELF_PATH..." + "$VSCODE_SERVER_PATCHELF_PATH" --set-interpreter "$VSCODE_SERVER_CUSTOM_GLIBC_LINKER" "$ROOT/node" + echo "Patching complete." +fi + "$ROOT/node" ${INSPECT:-} "$ROOT/out/server-main.js" "$@" diff --git a/resources/server/bin/helpers/check-requirements-linux-legacy.sh b/resources/server/bin/helpers/check-requirements-linux-legacy.sh deleted file mode 100755 index 0db77676965..00000000000 --- a/resources/server/bin/helpers/check-requirements-linux-legacy.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env sh -# -# Copyright (c) Microsoft Corporation. All rights reserved. -# - -set -e - -echo "!!! WARNING: Using legacy server, please check https://aka.ms/vscode-remote/faq/old-linux for additional information !!!" -exit 0 diff --git a/resources/server/bin/helpers/check-requirements-linux.sh b/resources/server/bin/helpers/check-requirements-linux.sh index 8ef07a2fb1f..8ea4c0b5adb 100644 --- a/resources/server/bin/helpers/check-requirements-linux.sh +++ b/resources/server/bin/helpers/check-requirements-linux.sh @@ -7,21 +7,20 @@ set -e # The script checks necessary server requirements for the classic server # scenarios. Currently, the script can exit with any of the following -# 3 exit codes and should be handled accordingly on the extension side. +# 2 exit codes and should be handled accordingly on the extension side. # # 0: All requirements are met, use the default server. # 99: Unsupported OS, abort server startup with appropriate error message. -# 100: Use legacy server. # # Do not remove this check. # Provides a way to skip the server requirements check from # outside the install flow. A system process can create this # file before the server is downloaded and installed. -if [ -f "/tmp/vscode-skip-server-requirements-check" ]; then - echo "!!! WARNING: Skipping server pre-requisite check !!!" - echo "!!! Server stability is not guaranteed. Proceed at your own risk. !!!" - exit 0 +if [ -f "/tmp/vscode-skip-server-requirements-check" ] || [ -n "$VSCODE_SERVER_CUSTOM_GLIBC_LINKER" ]; then + echo "!!! WARNING: Skipping server pre-requisite check !!!" + echo "!!! Server stability is not guaranteed. Proceed at your own risk. !!!" + exit 0 fi ARCH=$(uname -m) @@ -156,7 +155,7 @@ else fi if [ "$found_required_glibc" = "0" ] || [ "$found_required_glibcxx" = "0" ]; then - echo "Warning: Missing required dependencies. Please refer to our FAQ https://aka.ms/vscode-remote/faq/old-linux for additional information." + echo "Error: Missing required dependencies. Please refer to our FAQ https://aka.ms/vscode-remote/faq/old-linux for additional information." # Custom exit code based on https://tldp.org/LDP/abs/html/exitcodes.html - exit 100 + exit 99 fi diff --git a/src/main.ts b/src/main.ts index c132c9b8b9b..fdc424e1087 100644 --- a/src/main.ts +++ b/src/main.ts @@ -522,6 +522,18 @@ function getJSFlags(cliArgs: NativeParsedArgs): string | null { jsFlags.push(cliArgs['js-flags']); } + if (process.platform === 'linux') { + // Fix cppgc crash on Linux with 16KB page size. + // Refs https://issues.chromium.org/issues/378017037 + // The fix from https://github.com/electron/electron/commit/6c5b2ef55e08dc0bede02384747549c1eadac0eb + // only affects non-renderer process. + // The following will ensure that the flag will be + // applied to the renderer process as well. + // TODO(deepak1556): Remove this once we update to + // Chromium >= 134. + jsFlags.push('--nodecommit_pooled_pages'); + } + return jsFlags.length > 0 ? jsFlags.join(' ') : null; } diff --git a/src/vs/base/browser/fonts.ts b/src/vs/base/browser/fonts.ts index 73d028e7863..33f6a7af7b3 100644 --- a/src/vs/base/browser/fonts.ts +++ b/src/vs/base/browser/fonts.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isMacintosh, isWindows } from '../common/platform.js'; +import { mainWindow } from './window.js'; +import type { IJSONSchemaSnippet } from '../common/jsonSchema.js'; +import { isElectron, isMacintosh, isWindows } from '../common/platform.js'; /** * The best font-family to be used in CSS based on the platform: @@ -14,3 +16,34 @@ import { isMacintosh, isWindows } from '../common/platform.js'; * Note: this currently does not adjust for different locales. */ export const DEFAULT_FONT_FAMILY = isWindows ? '"Segoe WPC", "Segoe UI", sans-serif' : isMacintosh ? '-apple-system, BlinkMacSystemFont, sans-serif' : 'system-ui, "Ubuntu", "Droid Sans", sans-serif'; + +interface FontData { + readonly family: string; +} + +export const getFonts = async (): Promise => { + try { + // @ts-ignore + const fonts = await mainWindow.queryLocalFonts() as FontData[]; + const fontsArray = [...fonts]; + const families = fontsArray.map(font => font.family); + return families; + } catch (error) { + console.error(`Failed to query fonts: ${error}`); + return []; + } +}; + + +export const getFontSnippets = async (): Promise => { + if (!isElectron) { + return []; + } + const fonts = await getFonts(); + const snippets: IJSONSchemaSnippet[] = fonts.map(font => { + return { + body: `${font}` + }; + }); + return snippets; +}; diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 71af77b8f25..cf15d43b0ab 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -585,7 +585,7 @@ const unescapeInfo = new Map([ ['>', '>'], ]); -function createRenderer(): marked.Renderer { +function createPlainTextRenderer(): marked.Renderer { const renderer = new marked.Renderer(); renderer.code = ({ text }: marked.Tokens.Code): string => { @@ -647,10 +647,10 @@ function createRenderer(): marked.Renderer { }; return renderer; } -const plainTextRenderer = new Lazy(createRenderer); +const plainTextRenderer = new Lazy(createPlainTextRenderer); const plainTextWithCodeBlocksRenderer = new Lazy(() => { - const renderer = createRenderer(); + const renderer = createPlainTextRenderer(); renderer.code = ({ text }: marked.Tokens.Code): string => { return `\n\`\`\`\n${escape(text)}\n\`\`\`\n`; }; diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index f38ab6d17c0..872f328858f 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 60dba786a8a..ff25e129d89 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -71,6 +71,10 @@ white-space: normal; } +.monaco-dialog-box .dialog-message-row .dialog-message-container ul { + padding-inline-start: 20px; /* reduce excessive indent of list items in the dialog */ +} + /** Dialog: Message */ .monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message { line-height: 22px; diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 0c357a671e7..469df8e4a0d 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -14,7 +14,7 @@ import { Codicon } from '../../../common/codicons.js'; import { ThemeIcon } from '../../../common/themables.js'; import { KeyCode, KeyMod } from '../../../common/keyCodes.js'; import { mnemonicButtonLabel } from '../../../common/labels.js'; -import { Disposable } from '../../../common/lifecycle.js'; +import { Disposable, toDisposable } from '../../../common/lifecycle.js'; import { isLinux, isMacintosh, isWindows } from '../../../common/platform.js'; import './dialog.css'; import * as nls from '../../../../nls.js'; @@ -210,6 +210,7 @@ export class Dialog extends Disposable { }); return; }; + this._register(toDisposable(close)); const buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer)); const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId); diff --git a/src/vs/base/browser/ui/hover/hover.ts b/src/vs/base/browser/ui/hover/hover.ts index df0e343e3ef..6b877ec17fd 100644 --- a/src/vs/base/browser/ui/hover/hover.ts +++ b/src/vs/base/browser/ui/hover/hover.ts @@ -13,32 +13,6 @@ import type { IDisposable } from '../../../common/lifecycle.js'; * Enables the convenient display of rich markdown-based hovers in the workbench. */ export interface IHoverDelegate2 { - /** - * Shows a hover immediately, provided a hover with the same {@link options} object is not - * already visible. - * - * Use this method when you want to: - * - * - Control showing the hover yourself. - * - Show the hover immediately. - * - * @param options A set of options defining the characteristics of the hover. - * @param focus Whether to focus the hover (useful for keyboard accessibility). - * - * @example A simple usage with a single element target. - * - * ```typescript - * showHover({ - * text: new MarkdownString('Hello world'), - * target: someElement - * }); - * ``` - */ - showHover( - options: IHoverOptions, - focus?: boolean - ): IHoverWidget | undefined; - /** * Shows a hover after a delay, or immediately if the {@link groupId} matches the currently * shown hover. @@ -101,10 +75,36 @@ export interface IHoverDelegate2 { ): IDisposable; /** - * Hides the hover if it was visible. This call will be ignored if the hover is currently - * "locked" via the alt/option key. + * Shows a hover immediately, provided a hover with the same {@link options} object is not + * already visible. + * + * Use this method when you want to: + * + * - Control showing the hover yourself. + * - Show the hover immediately. + * + * @param options A set of options defining the characteristics of the hover. + * @param focus Whether to focus the hover (useful for keyboard accessibility). + * + * @example A simple usage with a single element target. + * + * ```typescript + * showInstantHover({ + * text: new MarkdownString('Hello world'), + * target: someElement + * }); + * ``` */ - hideHover(): void; + showInstantHover( + options: IHoverOptions, + focus?: boolean + ): IHoverWidget | undefined; + + /** + * Hides the hover if it was visible. This call will be ignored if the hover is currently + * "locked" via the alt/option key unless `force` is set. + */ + hideHover(force?: boolean): void; /** * This should only be used until we have the ability to show multiple context views @@ -116,8 +116,8 @@ export interface IHoverDelegate2 { * Sets up a managed hover for the given element. A managed hover will set up listeners for * mouse events, show the hover after a delay and provide hooks to easily update the content. * - * This should be used over {@link showHover} when fine-grained control is not needed. The - * managed hover also does not scale well, consider using {@link showHover} when showing hovers + * This should be used over {@link showInstantHover} when fine-grained control is not needed. The + * managed hover also does not scale well, consider using {@link showInstantHover} when showing hovers * for many elements. * * @param hoverDelegate The hover delegate containing hooks and configuration for the hover. @@ -387,7 +387,16 @@ export function isManagedHoverTooltipMarkdownString(obj: unknown): obj is IManag return typeof candidate === 'object' && 'markdown' in candidate && 'markdownNotSupportedFallback' in candidate; } -export type IManagedHoverContent = string | IManagedHoverTooltipMarkdownString | HTMLElement | undefined; +export interface IManagedHoverTooltipHTMLElement { + element: (token: CancellationToken) => HTMLElement | Promise; +} + +export function isManagedHoverTooltipHTMLElement(obj: unknown): obj is IManagedHoverTooltipHTMLElement { + const candidate = obj as IManagedHoverTooltipHTMLElement; + return typeof candidate === 'object' && 'element' in candidate; +} + +export type IManagedHoverContent = string | IManagedHoverTooltipMarkdownString | IManagedHoverTooltipHTMLElement | HTMLElement | undefined; export type IManagedHoverContentOrFactory = IManagedHoverContent | (() => IManagedHoverContent); export interface IManagedHoverOptions extends Pick { diff --git a/src/vs/base/browser/ui/hover/hoverDelegate2.ts b/src/vs/base/browser/ui/hover/hoverDelegate2.ts index 05b4c692d94..b49cb84951c 100644 --- a/src/vs/base/browser/ui/hover/hoverDelegate2.ts +++ b/src/vs/base/browser/ui/hover/hoverDelegate2.ts @@ -7,7 +7,7 @@ import { Disposable } from '../../../common/lifecycle.js'; import type { IHoverDelegate2 } from './hover.js'; let baseHoverDelegate: IHoverDelegate2 = { - showHover: () => undefined, + showInstantHover: () => undefined, showDelayedHover: () => undefined, setupDelayedHover: () => Disposable.None, setupDelayedHoverAtMouse: () => Disposable.None, diff --git a/src/vs/base/browser/ui/iconLabel/iconlabel.css b/src/vs/base/browser/ui/iconLabel/iconlabel.css index ad3a358bc5a..ed9278d7087 100644 --- a/src/vs/base/browser/ui/iconLabel/iconlabel.css +++ b/src/vs/base/browser/ui/iconLabel/iconlabel.css @@ -89,11 +89,6 @@ opacity: 0.66; } -/* make sure apply italic font style to decorations as well */ -.monaco-icon-label.italic::after { - font-style: italic; -} - .monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-name-container > .label-name, .monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-description-container > .label-description { text-decoration: line-through; diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 5ccf89beee4..4081c2f28b2 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -650,7 +650,7 @@ export class HistoryInputBox extends InputBox implements IHistoryNavigationWidge }, ' ({0} for history)', `\u21C5`); super(container, contextViewProvider, options); - this.history = new HistoryNavigator(options.history, 100); + this.history = this._register(new HistoryNavigator(options.history, 100)); // Function to append the history suffix to the placeholder if necessary const addSuffix = () => { diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 4c8af3e908e..67bad0de33f 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -1243,7 +1243,11 @@ export class ListView implements IListView { })); selectionStore.add(addDisposableListener(doc, 'selectionchange', () => { const selection = doc.getSelection(); - if (!selection) { + // if the selection changed _after_ mouseup, it's from clearing the list or similar, so teardown + if (!selection || selection.isCollapsed) { + if (movementStore.isDisposed) { + selectionStore.dispose(); + } return; } @@ -1527,8 +1531,9 @@ export class ListView implements IListView { protected getRenderRange(renderTop: number, renderHeight: number): IRange { const range = this.getVisibleRange(renderTop, renderHeight); if (this.currentSelectionBounds) { - range.start = Math.min(range.start, this.currentSelectionBounds.start); - range.end = Math.max(range.end, this.currentSelectionBounds.end + 1); + const max = this.rangeMap.count; + range.start = Math.min(range.start, this.currentSelectionBounds.start, max); + range.end = Math.min(Math.max(range.end, this.currentSelectionBounds.end + 1), max); } return range; diff --git a/src/vs/platform/severityIcon/browser/media/severityIcon.css b/src/vs/base/browser/ui/severityIcon/media/severityIcon.css similarity index 93% rename from src/vs/platform/severityIcon/browser/media/severityIcon.css rename to src/vs/base/browser/ui/severityIcon/media/severityIcon.css index 4fd1d06c80b..62d99edf337 100644 --- a/src/vs/platform/severityIcon/browser/media/severityIcon.css +++ b/src/vs/base/browser/ui/severityIcon/media/severityIcon.css @@ -8,7 +8,8 @@ .text-search-provider-messages .providerMessage .codicon.codicon-error, .extensions-viewlet > .extensions .codicon.codicon-error, .extension-editor .codicon.codicon-error, -.preferences-editor .codicon.codicon-error { +.preferences-editor .codicon.codicon-error, +.chat-attached-context-attachment .codicon.codicon-error { color: var(--vscode-problemsErrorIcon-foreground); } diff --git a/src/vs/platform/severityIcon/browser/severityIcon.ts b/src/vs/base/browser/ui/severityIcon/severityIcon.ts similarity index 82% rename from src/vs/platform/severityIcon/browser/severityIcon.ts rename to src/vs/base/browser/ui/severityIcon/severityIcon.ts index 73bfe9c2da1..66f5564139a 100644 --- a/src/vs/platform/severityIcon/browser/severityIcon.ts +++ b/src/vs/base/browser/ui/severityIcon/severityIcon.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import './media/severityIcon.css'; -import { Codicon } from '../../../base/common/codicons.js'; -import { ThemeIcon } from '../../../base/common/themables.js'; -import Severity from '../../../base/common/severity.js'; +import { Codicon } from '../../../common/codicons.js'; +import { ThemeIcon } from '../../../common/themables.js'; +import Severity from '../../../common/severity.js'; export namespace SeverityIcon { diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 75ab73a5354..1c2be8cc6a8 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -18,8 +18,6 @@ import * as nls from '../../../../nls.js'; import { IHoverDelegate } from '../hover/hoverDelegate.js'; import { createInstantHoverDelegate } from '../hover/hoverDelegateFactory.js'; - - export interface IToolBarOptions { orientation?: ActionsOrientation; actionViewItemProvider?: IActionViewItemProvider; diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index d4a40903443..98bf168f39d 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -620,6 +620,36 @@ function getActualStartIndex(array: T[], start: number): number { return start < 0 ? Math.max(start + array.length, 0) : Math.min(start, array.length); } +/** + * Utility that helps to pick a property from an object. + * + * ## Examples + * + * ```typescript + * interface IObject = { + * a: number, + * b: string, + * }; + * + * const list: IObject[] = [ + * { a: 1, b: 'foo' }, + * { a: 2, b: 'bar' }, + * ]; + * + * assert.deepStrictEqual( + * list.map(pick('a')), + * [1, 2], + * ); + * ``` + */ +export const pick = ( + key: TKeyName, +) => { + return (obj: TObject): TObject[TKeyName] => { + return obj[key]; + }; +}; + /** * When comparing two values, * a negative number indicates that the first value is less than the second, diff --git a/src/vs/base/common/dataTransfer.ts b/src/vs/base/common/dataTransfer.ts index dd3f31f6e79..3ce5770c57f 100644 --- a/src/vs/base/common/dataTransfer.ts +++ b/src/vs/base/common/dataTransfer.ts @@ -16,22 +16,25 @@ export interface IDataTransferFile { } export interface IDataTransferItem { + id?: string; asString(): Thenable; asFile(): IDataTransferFile | undefined; value: any; } -export function createStringDataTransferItem(stringOrPromise: string | Promise): IDataTransferItem { +export function createStringDataTransferItem(stringOrPromise: string | Promise, id?: string): IDataTransferItem { return { + id, asString: async () => stringOrPromise, asFile: () => undefined, value: typeof stringOrPromise === 'string' ? stringOrPromise : undefined, }; } -export function createFileDataTransferItem(fileName: string, uri: URI | undefined, data: () => Promise): IDataTransferItem { +export function createFileDataTransferItem(fileName: string, uri: URI | undefined, data: () => Promise, id?: string): IDataTransferItem { const file = { id: generateUuid(), name: fileName, uri, data }; return { + id, asString: async () => '', asFile: () => file, value: undefined, diff --git a/src/vs/base/common/history.ts b/src/vs/base/common/history.ts index a705839b71e..3c23e8e3a29 100644 --- a/src/vs/base/common/history.ts +++ b/src/vs/base/common/history.ts @@ -5,6 +5,7 @@ import { SetWithKey } from './collections.js'; import { Event } from './event.js'; +import { IDisposable } from './lifecycle.js'; import { ArrayNavigator, INavigator } from './navigator.js'; export interface IHistory { @@ -20,6 +21,7 @@ export interface IHistory { export class HistoryNavigator implements INavigator { private _limit: number; private _navigator!: ArrayNavigator; + private _disposable: IDisposable | undefined; constructor( private _history: IHistory = new Set(), @@ -28,7 +30,7 @@ export class HistoryNavigator implements INavigator { this._limit = limit; this._onChange(); if (this._history.onDidChange) { - this._history.onDidChange(() => this._onChange()); + this._disposable = this._history.onDidChange(() => this._onChange()); } } @@ -119,6 +121,13 @@ export class HistoryNavigator implements INavigator { this._history.forEach(e => elements.push(e)); return elements; } + + public dispose(): void { + if (this._disposable) { + this._disposable.dispose(); + this._disposable = undefined; + } + } } interface HistoryNode { diff --git a/src/vs/base/common/hotReload.ts b/src/vs/base/common/hotReload.ts index 80901b470f0..4f0e1341681 100644 --- a/src/vs/base/common/hotReload.ts +++ b/src/vs/base/common/hotReload.ts @@ -6,12 +6,8 @@ import { IDisposable } from './lifecycle.js'; import { env } from './process.js'; -function hotReloadDisabled() { - return true; // TODO@hediet fix hot reload. -} - export function isHotReloadEnabled(): boolean { - return !hotReloadDisabled() && env && !!env['VSCODE_DEV']; + return env && !!env['VSCODE_DEV_DEBUG']; } export function registerHotReloadHandler(handler: HotReloadHandler): IDisposable { if (!isHotReloadEnabled()) { diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index e04e62f76d3..036ff73392e 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -827,3 +827,19 @@ export class DisposableMap implements ID return this._store[Symbol.iterator](); } } + +/** + * Call `then` on a Promise, unless the returned disposable is disposed. + */ +export function thenIfNotDisposed(promise: Promise, then: (result: T) => void): IDisposable { + let disposed = false; + promise.then(result => { + if (disposed) { + return; + } + then(result); + }); + return toDisposable(() => { + disposed = true; + }); +} diff --git a/src/vs/base/common/observableInternal/autorun.ts b/src/vs/base/common/observableInternal/autorun.ts index b1f8f4e9530..03c37aa6c19 100644 --- a/src/vs/base/common/observableInternal/autorun.ts +++ b/src/vs/base/common/observableInternal/autorun.ts @@ -168,13 +168,13 @@ export const enum AutorunState { } export class AutorunObserver implements IObserver, IReader, IDisposable { - public _state = AutorunState.stale; - private updateCount = 0; - private disposed = false; - public _dependencies = new Set>(); - private dependenciesToBeRemoved = new Set>(); - private changeSummary: TChangeSummary | undefined; - public _isRunning = false; + private _state = AutorunState.stale; + private _updateCount = 0; + private _disposed = false; + private _dependencies = new Set>(); + private _dependenciesToBeRemoved = new Set>(); + private _changeSummary: TChangeSummary | undefined; + private _isRunning = false; public get debugName(): string { return this._debugNameData.getDebugName(this) ?? '(anonymous)'; @@ -186,7 +186,7 @@ export class AutorunObserver implements IObserver, IReader private readonly createChangeSummary: (() => TChangeSummary) | undefined, private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined, ) { - this.changeSummary = this.createChangeSummary?.(); + this._changeSummary = this.createChangeSummary?.(); getLogger()?.handleAutorunCreated(this); this._run(); @@ -194,7 +194,7 @@ export class AutorunObserver implements IObserver, IReader } public dispose(): void { - this.disposed = true; + this._disposed = true; for (const o of this._dependencies) { o.removeObserver(this); // Warning: external call! } @@ -205,18 +205,18 @@ export class AutorunObserver implements IObserver, IReader } private _run() { - const emptySet = this.dependenciesToBeRemoved; - this.dependenciesToBeRemoved = this._dependencies; + const emptySet = this._dependenciesToBeRemoved; + this._dependenciesToBeRemoved = this._dependencies; this._dependencies = emptySet; this._state = AutorunState.upToDate; try { - if (!this.disposed) { + if (!this._disposed) { getLogger()?.handleAutorunStarted(this); - const changeSummary = this.changeSummary!; + const changeSummary = this._changeSummary!; try { - this.changeSummary = this.createChangeSummary?.(); // Warning: external call! + this._changeSummary = this.createChangeSummary?.(); // Warning: external call! this._isRunning = true; this._runFn(this, changeSummary); // Warning: external call! } catch (e) { @@ -226,15 +226,15 @@ export class AutorunObserver implements IObserver, IReader } } } finally { - if (!this.disposed) { + if (!this._disposed) { getLogger()?.handleAutorunFinished(this); } // We don't want our observed observables to think that they are (not even temporarily) not being observed. // Thus, we only unsubscribe from observables that are definitely not read anymore. - for (const o of this.dependenciesToBeRemoved) { + for (const o of this._dependenciesToBeRemoved) { o.removeObserver(this); // Warning: external call! } - this.dependenciesToBeRemoved.clear(); + this._dependenciesToBeRemoved.clear(); } } @@ -247,12 +247,12 @@ export class AutorunObserver implements IObserver, IReader if (this._state === AutorunState.upToDate) { this._state = AutorunState.dependenciesMightHaveChanged; } - this.updateCount++; + this._updateCount++; } public endUpdate(_observable: IObservable): void { try { - if (this.updateCount === 1) { + if (this._updateCount === 1) { do { if (this._state === AutorunState.dependenciesMightHaveChanged) { this._state = AutorunState.upToDate; @@ -271,10 +271,10 @@ export class AutorunObserver implements IObserver, IReader } while (this._state !== AutorunState.upToDate); } } finally { - this.updateCount--; + this._updateCount--; } - assertFn(() => this.updateCount >= 0); + assertFn(() => this._updateCount >= 0); } public handlePossibleChange(observable: IObservable): void { @@ -292,7 +292,7 @@ export class AutorunObserver implements IObserver, IReader changedObservable: observable, change, didChange: (o): this is any => o === observable as any, - }, this.changeSummary!) : true; + }, this._changeSummary!) : true; if (shouldReact) { this._state = AutorunState.stale; } @@ -303,7 +303,7 @@ export class AutorunObserver implements IObserver, IReader } private _isDependency(observable: IObservableWithChange): boolean { - return this._dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable); + return this._dependencies.has(observable) && !this._dependenciesToBeRemoved.has(observable); } // IReader implementation @@ -312,16 +312,33 @@ export class AutorunObserver implements IObserver, IReader if (!this._isRunning) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); } // In case the run action disposes the autorun - if (this.disposed) { + if (this._disposed) { return observable.get(); // warning: external call! } observable.addObserver(this); // warning: external call! const value = observable.get(); // warning: external call! this._dependencies.add(observable); - this.dependenciesToBeRemoved.delete(observable); + this._dependenciesToBeRemoved.delete(observable); return value; } + + public debugGetState() { + return { + isRunning: this._isRunning, + updateCount: this._updateCount, + dependencies: this._dependencies, + state: this._state, + }; + } + + public debugRerun(): void { + if (!this._isRunning) { + this._run(); + } else { + this._state = AutorunState.stale; + } + } } export namespace autorun { diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 31ce4988a13..4d7d4ea9e19 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -290,7 +290,7 @@ export abstract class ConvenientObservable implements IObservableWit } export abstract class BaseObservable extends ConvenientObservable { - public readonly _observers = new Set(); + protected readonly _observers = new Set(); constructor() { super(); @@ -329,6 +329,10 @@ export abstract class BaseObservable extends ConvenientObserv } return this; } + + public debugGetObservers() { + return this._observers; + } } /** @@ -385,7 +389,7 @@ export function subtransaction(tx: ITransaction | undefined, fn: (tx: ITransacti } export class TransactionImpl implements ITransaction { - public _updatingObservers: { observer: IObserver; observable: IObservable }[] | null = []; + private _updatingObservers: { observer: IObserver; observable: IObservable }[] | null = []; constructor(public readonly _fn: Function, private readonly _getDebugName?: () => string) { getLogger()?.handleBeginTransaction(this); @@ -414,6 +418,10 @@ export class TransactionImpl implements ITransaction { this._updatingObservers = null; getLogger()?.handleEndTransaction(this); } + + public debugGetUpdatingObservers() { + return this._updatingObservers; + } } /** @@ -495,6 +503,16 @@ export class ObservableValue protected _setValue(newValue: T): void { this._value = newValue; } + + public debugGetState() { + return { + value: this._value, + }; + } + + public debugSetValue(value: unknown) { + this._value = value as T; + } } /** diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index 0735be62c23..59d8dc725a7 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -194,14 +194,14 @@ export const enum DerivedState { } export class Derived extends BaseObservable implements IReader, IObserver { - public _state = DerivedState.initial; - private value: T | undefined = undefined; - public _updateCount = 0; - public _dependencies = new Set>(); - private dependenciesToBeRemoved = new Set>(); - private changeSummary: TChangeSummary | undefined = undefined; + private _state = DerivedState.initial; + private _value: T | undefined = undefined; + private _updateCount = 0; + private _dependencies = new Set>(); + private _dependenciesToBeRemoved = new Set>(); + private _changeSummary: TChangeSummary | undefined = undefined; private _isUpdating = false; - public _isComputing = false; + private _isComputing = false; public override get debugName(): string { return this._debugNameData.getDebugName(this) ?? '(anonymous)'; @@ -216,7 +216,7 @@ export class Derived extends BaseObservable im private readonly _equalityComparator: EqualityComparer, ) { super(); - this.changeSummary = this.createChangeSummary?.(); + this._changeSummary = this.createChangeSummary?.(); } protected override onLastObserverRemoved(): void { @@ -225,7 +225,7 @@ export class Derived extends BaseObservable im * that our cache is invalid. */ this._state = DerivedState.initial; - this.value = undefined; + this._value = undefined; getLogger()?.handleDerivedCleared(this); for (const d of this._dependencies) { d.removeObserver(this); @@ -283,17 +283,17 @@ export class Derived extends BaseObservable im } // In case recomputation changed one of our dependencies, we need to recompute again. } while (this._state !== DerivedState.upToDate); - return this.value!; + return this._value!; } } private _recompute() { - const emptySet = this.dependenciesToBeRemoved; - this.dependenciesToBeRemoved = this._dependencies; + const emptySet = this._dependenciesToBeRemoved; + this._dependenciesToBeRemoved = this._dependencies; this._dependencies = emptySet; const hadValue = this._state !== DerivedState.initial; - const oldValue = this.value; + const oldValue = this._value; this._state = DerivedState.upToDate; let didChange = false; @@ -301,27 +301,27 @@ export class Derived extends BaseObservable im this._isComputing = true; try { - const changeSummary = this.changeSummary!; - this.changeSummary = this.createChangeSummary?.(); + const changeSummary = this._changeSummary!; + this._changeSummary = this.createChangeSummary?.(); try { this._isReaderValid = true; /** might call {@link handleChange} indirectly, which could invalidate us */ - this.value = this._computeFn(this, changeSummary); + this._value = this._computeFn(this, changeSummary); } finally { this._isReaderValid = false; // We don't want our observed observables to think that they are (not even temporarily) not being observed. // Thus, we only unsubscribe from observables that are definitely not read anymore. - for (const o of this.dependenciesToBeRemoved) { + for (const o of this._dependenciesToBeRemoved) { o.removeObserver(this); } - this.dependenciesToBeRemoved.clear(); + this._dependenciesToBeRemoved.clear(); } - didChange = hadValue && !(this._equalityComparator(oldValue!, this.value)); + didChange = hadValue && !(this._equalityComparator(oldValue!, this._value)); getLogger()?.handleObservableUpdated(this, { oldValue, - newValue: this.value, + newValue: this._value, change: undefined, didChange, hadValue, @@ -396,7 +396,7 @@ export class Derived extends BaseObservable im public handlePossibleChange(observable: IObservable): void { // In all other states, observers already know that we might have changed. - if (this._state === DerivedState.upToDate && this._dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) { + if (this._state === DerivedState.upToDate && this._dependencies.has(observable) && !this._dependenciesToBeRemoved.has(observable)) { this._state = DerivedState.dependenciesMightHaveChanged; for (const r of this._observers) { r.handlePossibleChange(this); @@ -405,7 +405,7 @@ export class Derived extends BaseObservable im } public handleChange(observable: IObservableWithChange, change: TChange): void { - if (this._dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) { + if (this._dependencies.has(observable) && !this._dependenciesToBeRemoved.has(observable)) { getLogger()?.handleDerivedDependencyChanged(this, observable, change); let shouldReact = false; @@ -414,7 +414,7 @@ export class Derived extends BaseObservable im changedObservable: observable, change, didChange: (o): this is any => o === observable as any, - }, this.changeSummary!) : true; + }, this._changeSummary!) : true; } catch (e) { onBugIndicatingError(e); } @@ -443,7 +443,7 @@ export class Derived extends BaseObservable im const value = observable.get(); // Which is why we only add the observable to the dependencies now. this._dependencies.add(observable); - this.dependenciesToBeRemoved.delete(observable); + this._dependenciesToBeRemoved.delete(observable); return value; } @@ -469,6 +469,21 @@ export class Derived extends BaseObservable im } super.removeObserver(observer); } + + public debugGetState() { + return { + state: this._state, + updateCount: this._updateCount, + isComputing: this._isComputing, + dependencies: this._dependencies, + value: this._value, + }; + } + + public debugSetValue(newValue: unknown) { + this._value = newValue as any; + } + } diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index 8fe37f4c48a..c86501e702a 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -16,6 +16,8 @@ export { type DebugOwner } from './debugName.js'; import { addLogger, setLogObservableFn } from './logging/logging.js'; import { ConsoleObservableLogger, logObservableToConsole } from './logging/consoleObservableLogger.js'; +import { DevToolsLogger } from './logging/debugger/devToolsLogger.js'; +import { env } from '../process.js'; setLogObservableFn(logObservableToConsole); @@ -27,3 +29,8 @@ const enableLogging = false if (enableLogging) { addLogger(new ConsoleObservableLogger()); } + +if (env && env['VSCODE_DEV_DEBUG']) { + // To debug observables you also need the extension "ms-vscode.debug-value-editor" + addLogger(DevToolsLogger.getInstance()); +} diff --git a/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts b/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts index e0b5fae1820..138732f44b2 100644 --- a/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts +++ b/src/vs/base/common/observableInternal/logging/debugger/debuggerApi.d.ts @@ -23,6 +23,9 @@ export type ObsDebuggerApi = { getSummarizedInstances(): IObsPushState; getDerivedInfo(instanceId: ObsInstanceId): IDerivedObservableDetailedInfo; getAutorunInfo(instanceId: ObsInstanceId): IAutorunDetailedInfo; + getObservableValueInfo(instanceId: ObsInstanceId): IObservableValueInfo; + setValue(instanceId: ObsInstanceId, jsonValue: unknown): void; + getValue(instanceId: ObsInstanceId): unknown; getTransactionState(): ITransactionState | undefined; } @@ -94,6 +97,10 @@ export type ObsStateUpdate = Partial & DeepPartial = { [TKey in keyof T]?: DeepPartial }; +export interface IObservableValueInfo { + observers: IObsInstanceRef[]; +} + export interface IDerivedObservableDetailedInfo { dependencies: IObsInstanceRef[]; observers: IObsInstanceRef[]; diff --git a/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts b/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts index a85329ca8c9..208c623cf7e 100644 --- a/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts +++ b/src/vs/base/common/observableInternal/logging/debugger/devToolsLogger.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AutorunObserver, AutorunState } from '../../autorun.js'; -import { IObservable, IObserver, TransactionImpl } from '../../base.js'; +import { BaseObservable, IObservable, IObserver, ObservableValue, TransactionImpl } from '../../base.js'; import { Derived, DerivedState } from '../../derived.js'; import { IChangeInformation, IObservableLogger } from '../logging.js'; import { formatValue } from '../consoleObservableLogger.js'; @@ -12,6 +12,8 @@ import { ObsDebuggerApi, IObsDeclaration, ObsInstanceId, ObsStateUpdate, ITransa import { registerDebugChannel } from './debuggerRpc.js'; import { deepAssign, deepAssignDeleteNulls, getFirstStackFrameOutsideOf, ILocation, Throttler } from './utils.js'; import { isDefined } from '../../../types.js'; +import { FromEventObservable } from '../../utils.js'; +import { BugIndicatingError, onUnexpectedError } from '../../../errors.js'; interface IInstanceInfo { declarationId: number; @@ -75,21 +77,61 @@ export class DevToolsLogger implements IObservableLogger { getSummarizedInstances: () => { return null!; }, + getObservableValueInfo: instanceId => { + const obs = this._aliveInstances.get(instanceId) as BaseObservable; + return { + observers: [...obs.debugGetObservers()].map(d => this._formatObserver(d)).filter(isDefined), + }; + }, getDerivedInfo: instanceId => { const d = this._aliveInstances.get(instanceId) as Derived; return { - dependencies: [...d._dependencies].map(d => this._formatObservable(d)), - observers: [...d._observers].map(d => this._formatObserver(d)).filter(isDefined), + dependencies: [...d.debugGetState().dependencies].map(d => this._formatObservable(d)).filter(isDefined), + observers: [...d.debugGetObservers()].map(d => this._formatObserver(d)).filter(isDefined), }; }, getAutorunInfo: instanceId => { const obs = this._aliveInstances.get(instanceId) as AutorunObserver; return { - dependencies: [...obs._dependencies].map(d => this._formatObservable(d)), + dependencies: [...obs.debugGetState().dependencies].map(d => this._formatObservable(d)).filter(isDefined), }; }, getTransactionState: () => { return this.getTransactionState(); + }, + setValue: (instanceId, jsonValue) => { + const obs = this._aliveInstances.get(instanceId) as BaseObservable; + + if (obs instanceof Derived) { + obs.debugSetValue(jsonValue); + } else if (obs instanceof ObservableValue) { + obs.debugSetValue(jsonValue); + } else if (obs instanceof FromEventObservable) { + obs.debugSetValue(jsonValue); + } else { + throw new BugIndicatingError('Observable is not supported'); + } + + const observers = [...obs.debugGetObservers()]; + for (const d of observers) { + d.beginUpdate(obs); + } + for (const d of observers) { + d.handleChange(obs, undefined); + } + for (const d of observers) { + d.endUpdate(obs); + } + }, + getValue: instanceId => { + const obs = this._aliveInstances.get(instanceId) as BaseObservable; + if (obs instanceof Derived) { + return formatValue(obs.debugGetState().value, 200); + } else if (obs instanceof ObservableValue) { + return formatValue(obs.debugGetState().value, 200); + } + + return undefined; } } }; @@ -101,7 +143,7 @@ export class DevToolsLogger implements IObservableLogger { if (txs.length === 0) { return undefined; } - const observerQueue = txs.flatMap(t => t._updatingObservers ?? []).map(o => o.observer); + const observerQueue = txs.flatMap(t => t.debugGetUpdatingObservers() ?? []).map(o => o.observer); const processedObservers = new Set(); while (observerQueue.length > 0) { const observer = observerQueue.shift()!; @@ -124,36 +166,42 @@ export class DevToolsLogger implements IObservableLogger { return { names: txs.map(t => t.getDebugName() ?? 'tx'), affected }; } - private _getObservableInfo(observable: IObservable): IObservableInfo { + private _getObservableInfo(observable: IObservable): IObservableInfo | undefined { const info = this._instanceInfos.get(observable); if (!info) { - throw new Error('No info found'); + onUnexpectedError(new BugIndicatingError('No info found')); + return undefined; } return info as IObservableInfo; } - private _getAutorunInfo(autorun: AutorunObserver): IAutorunInfo { + private _getAutorunInfo(autorun: AutorunObserver): IAutorunInfo | undefined { const info = this._instanceInfos.get(autorun); if (!info) { - throw new Error('No info found'); + onUnexpectedError(new BugIndicatingError('No info found')); + return undefined; } return info as IAutorunInfo; } private _getInfo(observer: IObserver, queue: (observer: IObserver) => void): ObserverInstanceState | undefined { if (observer instanceof Derived) { - const observersToUpdate = [...observer._observers]; + const observersToUpdate = [...observer.debugGetObservers()]; for (const o of observersToUpdate) { queue(o); } - const info = this._getObservableInfo(observer)!; - const base = { name: observer.debugName, instanceId: info.instanceId, updateCount: observer._updateCount }; - const changedDependencies = [...info.changedObservables].map(o => this._instanceInfos.get(o)!.instanceId); - if (observer._isComputing) { + const info = this._getObservableInfo(observer); + if (!info) { return; } + + const observerState = observer.debugGetState(); + + const base = { name: observer.debugName, instanceId: info.instanceId, updateCount: observerState.updateCount }; + const changedDependencies = [...info.changedObservables].map(o => this._instanceInfos.get(o)?.instanceId).filter(isDefined); + if (observerState.isComputing) { return { ...base, type: 'observable/derived', state: 'updating', changedDependencies, initialComputation: false }; } - switch (observer._state) { + switch (observerState.state) { case DerivedState.initial: return { ...base, type: 'observable/derived', state: 'noValue' }; case DerivedState.upToDate: @@ -164,13 +212,15 @@ export class DevToolsLogger implements IObservableLogger { return { ...base, type: 'observable/derived', state: 'possiblyStale' }; } } else if (observer instanceof AutorunObserver) { - const info = this._getAutorunInfo(observer)!; + const info = this._getAutorunInfo(observer); + if (!info) { return undefined; } + const base = { name: observer.debugName, instanceId: info.instanceId, updateCount: info.updateCount }; const changedDependencies = [...info.changedObservables].map(o => this._instanceInfos.get(o)!.instanceId); - if (observer._isRunning) { + if (observer.debugGetState().isRunning) { return { ...base, type: 'autorun', state: 'updating', changedDependencies }; } - switch (observer._state) { + switch (observer.debugGetState().state) { case AutorunState.upToDate: return { ...base, type: 'autorun', state: 'upToDate' }; case AutorunState.stale: @@ -183,8 +233,10 @@ export class DevToolsLogger implements IObservableLogger { return undefined; } - private _formatObservable(obs: IObservable): { name: string; instanceId: ObsInstanceId } { - return { name: obs.debugName, instanceId: this._getObservableInfo(obs)?.instanceId! }; + private _formatObservable(obs: IObservable): { name: string; instanceId: ObsInstanceId } | undefined { + const info = this._getObservableInfo(obs); + if (!info) { return undefined; } + return { name: obs.debugName, instanceId: info.instanceId }; } private _formatObserver(obs: IObserver): { name: string; instanceId: ObsInstanceId } | undefined { @@ -230,16 +282,18 @@ export class DevToolsLogger implements IObservableLogger { let shallow = true; let loc!: ILocation; - while (true) { - const l = Error.stackTraceLimit; - Error.stackTraceLimit = shallow ? 6 : 20; - const stack = new Error().stack!; - Error.stackTraceLimit = l; + const Err = Error as any as { stackTraceLimit: number }; // For the monaco editor checks, which don't have the nodejs types. - let result = getFirstStackFrameOutsideOf(stack, /[/\\]observableInternal[/\\]|[/\\]util(s)?\./); + while (true) { + const l = Err.stackTraceLimit; + Err.stackTraceLimit = shallow ? 6 : 20; + const stack = new Error().stack!; + Err.stackTraceLimit = l; + + let result = getFirstStackFrameOutsideOf(stack, /[/\\]observableInternal[/\\]|\.observe|[/\\]util(s)?\./); if (!shallow && !result) { - result = getFirstStackFrameOutsideOf(stack, /[/\\]observableInternal[/\\]/)!; + result = getFirstStackFrameOutsideOf(stack, /[/\\]observableInternal[/\\]|\.observe/)!; } if (result) { loc = result; @@ -248,6 +302,7 @@ export class DevToolsLogger implements IObservableLogger { if (!shallow) { console.error('Could not find location for declaration', new Error().stack); loc = { fileName: 'unknown', line: 0, column: 0, id: 'unknown' }; + break; } shallow = false; } @@ -284,30 +339,30 @@ export class DevToolsLogger implements IObservableLogger { handleOnListenerCountChanged(observable: IObservable, newCount: number): void { const info = this._getObservableInfo(observable); - if (info) { - if (info.listenerCount === 0 && newCount > 0) { - const type: IObsDeclaration['type'] = - observable instanceof Derived ? 'observable/derived' : 'observable/value'; - this._aliveInstances.set(info.instanceId, observable); - this._handleChange({ - instances: { - [info.instanceId]: { - instanceId: info.instanceId, - declarationId: info.declarationId, - formattedValue: info.lastValue, - type, - name: observable.debugName, - } + if (!info) { return; } + + if (info.listenerCount === 0 && newCount > 0) { + const type: IObsDeclaration['type'] = + observable instanceof Derived ? 'observable/derived' : 'observable/value'; + this._aliveInstances.set(info.instanceId, observable); + this._handleChange({ + instances: { + [info.instanceId]: { + instanceId: info.instanceId, + declarationId: info.declarationId, + formattedValue: info.lastValue, + type, + name: observable.debugName, } - }); - } else if (info.listenerCount > 0 && newCount === 0) { - this._handleChange({ - instances: { [info.instanceId]: null } - }); - this._aliveInstances.delete(info.instanceId); - } - info.listenerCount = newCount; + } + }); + } else if (info.listenerCount > 0 && newCount === 0) { + this._handleChange({ + instances: { [info.instanceId]: null } + }); + this._aliveInstances.delete(info.instanceId); } + info.listenerCount = newCount; } handleObservableUpdated(observable: IObservable, changeInfo: IChangeInformation): void { @@ -355,32 +410,32 @@ export class DevToolsLogger implements IObservableLogger { } handleAutorunDisposed(autorun: AutorunObserver): void { const info = this._getAutorunInfo(autorun); - if (info) { - this._handleChange({ - instances: { [info.instanceId]: null } - }); - this._instanceInfos.delete(autorun); - this._aliveInstances.delete(info.instanceId); - } + if (!info) { return; } + + this._handleChange({ + instances: { [info.instanceId]: null } + }); + this._instanceInfos.delete(autorun); + this._aliveInstances.delete(info.instanceId); } handleAutorunDependencyChanged(autorun: AutorunObserver, observable: IObservable, change: unknown): void { const info = this._getAutorunInfo(autorun); - if (info) { - info.changedObservables.add(observable); - } + if (!info) { return; } + + info.changedObservables.add(observable); } handleAutorunStarted(autorun: AutorunObserver): void { } handleAutorunFinished(autorun: AutorunObserver): void { const info = this._getAutorunInfo(autorun); - if (info) { - info.changedObservables.clear(); - info.updateCount++; - this._handleChange({ - instances: { [info.instanceId]: { runCount: info.updateCount } } - }); - } + if (!info) { return; } + + info.changedObservables.clear(); + info.updateCount++; + this._handleChange({ + instances: { [info.instanceId]: { runCount: info.updateCount } } + }); } handleDerivedDependencyChanged(derived: Derived, observable: IObservable, change: unknown): void { @@ -391,33 +446,33 @@ export class DevToolsLogger implements IObservableLogger { } _handleDerivedRecomputed(observable: Derived, changeInfo: IChangeInformation): void { const info = this._getObservableInfo(observable); - if (info) { - const formattedValue = formatValue(changeInfo.newValue, 30); - info.updateCount++; - info.changedObservables.clear(); + if (!info) { return; } - info.lastValue = formattedValue; - if (info.listenerCount > 0) { - this._handleChange({ - instances: { [info.instanceId]: { formattedValue: formattedValue, recomputationCount: info.updateCount } } - }); - } + const formattedValue = formatValue(changeInfo.newValue, 30); + info.updateCount++; + info.changedObservables.clear(); + + info.lastValue = formattedValue; + if (info.listenerCount > 0) { + this._handleChange({ + instances: { [info.instanceId]: { formattedValue: formattedValue, recomputationCount: info.updateCount } } + }); } } handleDerivedCleared(observable: Derived): void { const info = this._getObservableInfo(observable); - if (info) { - info.lastValue = undefined; - info.changedObservables.clear(); - if (info.listenerCount > 0) { - this._handleChange({ - instances: { - [info.instanceId]: { - formattedValue: undefined, - } + if (!info) { return; } + + info.lastValue = undefined; + info.changedObservables.clear(); + if (info.listenerCount > 0) { + this._handleChange({ + instances: { + [info.instanceId]: { + formattedValue: undefined, } - }); - } + } + }); } } handleBeginTransaction(transaction: TransactionImpl): void { diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index 3b5bf7d35bf..530474b334e 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/src/vs/base/common/observableInternal/utils.ts @@ -102,9 +102,9 @@ export function observableFromEventOpts( export class FromEventObservable extends BaseObservable { public static globalTransaction: ITransaction | undefined; - private value: T | undefined; - private hasValue = false; - private subscription: IDisposable | undefined; + private _value: T | undefined; + private _hasValue = false; + private _subscription: IDisposable | undefined; constructor( private readonly _debugNameData: DebugNameData, @@ -126,25 +126,25 @@ export class FromEventObservable extends BaseObservable { } protected override onFirstObserverAdded(): void { - this.subscription = this.event(this.handleEvent); + this._subscription = this.event(this.handleEvent); } private readonly handleEvent = (args: TArgs | undefined) => { const newValue = this._getValue(args); - const oldValue = this.value; + const oldValue = this._value; - const didChange = !this.hasValue || !(this._equalityComparator(oldValue!, newValue)); + const didChange = !this._hasValue || !(this._equalityComparator(oldValue!, newValue)); let didRunTransaction = false; if (didChange) { - this.value = newValue; + this._value = newValue; - if (this.hasValue) { + if (this._hasValue) { didRunTransaction = true; subtransaction( this._getTransaction(), (tx) => { - getLogger()?.handleObservableUpdated(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue }); + getLogger()?.handleObservableUpdated(this, { oldValue, newValue, change: undefined, didChange, hadValue: this._hasValue }); for (const o of this._observers) { tx.updateObserver(o, this); @@ -157,33 +157,37 @@ export class FromEventObservable extends BaseObservable { } ); } - this.hasValue = true; + this._hasValue = true; } if (!didRunTransaction) { - getLogger()?.handleObservableUpdated(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue }); + getLogger()?.handleObservableUpdated(this, { oldValue, newValue, change: undefined, didChange, hadValue: this._hasValue }); } }; protected override onLastObserverRemoved(): void { - this.subscription!.dispose(); - this.subscription = undefined; - this.hasValue = false; - this.value = undefined; + this._subscription!.dispose(); + this._subscription = undefined; + this._hasValue = false; + this._value = undefined; } public get(): T { - if (this.subscription) { - if (!this.hasValue) { + if (this._subscription) { + if (!this._hasValue) { this.handleEvent(undefined); } - return this.value!; + return this._value!; } else { // no cache, as there are no subscribers to keep it updated const value = this._getValue(undefined); return value; } } + + public debugSetValue(value: unknown) { + this._value = value as any; + } } export namespace observableFromEvent { diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 52c4c943d97..0527af13b60 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -95,11 +95,9 @@ export interface IProductConfiguration { readonly extensionsGallery?: { readonly serviceUrl: string; - readonly itemUrl: string; - readonly publisherUrl: string; - readonly resourceUrlTemplate: string; - readonly extensionUrlTemplate: string; readonly controlUrl: string; + readonly extensionUrlTemplate: string; + readonly resourceUrlTemplate: string; readonly nlsBaseUrl: string; }; @@ -188,6 +186,7 @@ export interface IProductConfiguration { readonly 'editSessions.store'?: Omit; readonly darwinUniversalAssetId?: string; + readonly darwinBundleIdentifier?: string; readonly profileTemplatesUrl?: string; readonly commonlyUsedSettings?: string[]; @@ -319,10 +318,21 @@ export interface IDefaultChatAgent { readonly providerName: string; readonly enterpriseProviderId: string; readonly enterpriseProviderName: string; - readonly providerSetting: string; readonly providerUriSetting: string; readonly providerScopes: string[][]; readonly entitlementUrl: string; readonly entitlementSignupLimitedUrl: string; + + readonly chatQuotaExceededContext: string; + readonly completionsQuotaExceededContext: string; + + readonly walkthroughCommand: string; + readonly completionsMenuCommand: string; + readonly completionsRefreshTokenCommand: string; + readonly chatRefreshTokenCommand: string; + + readonly completionsAdvancedSetting: string; + readonly completionsEnablementSetting: string; + readonly nextEditSuggestionsSetting: string; } diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 9ed7eaa4732..1a8467170b5 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -802,14 +802,20 @@ export function rcut(text: string, n: number, suffix = ''): string { return result.trim().replace(/b$/, '') + suffix; } -// Escape codes, compiled from https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ -// Plus additional markers for custom `\x1b]...\x07` instructions. -const CSI_SEQUENCE = /(?:(?:\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~])|(:?\x1b\].*?\x07)/g; +// Defacto standard: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html +const CSI_SEQUENCE = /(?:\x1b\[|\x9b)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~]/; +const OSC_SEQUENCE = /(?:\x1b\]|\x9d).*?(?:\x1b\\|\x07|\x9c)/; +const ESC_SEQUENCE = /\x1b(?:[ #%\(\)\*\+\-\.\/]?[a-zA-Z0-9\|}~@])/; +const CONTROL_SEQUENCES = new RegExp('(?:' + [ + CSI_SEQUENCE.source, + OSC_SEQUENCE.source, + ESC_SEQUENCE.source, +].join('|') + ')', 'g'); /** Iterates over parts of a string with CSI sequences */ export function* forAnsiStringParts(str: string) { let last = 0; - for (const match of str.matchAll(CSI_SEQUENCE)) { + for (const match of str.matchAll(CONTROL_SEQUENCES)) { if (last !== match.index) { yield { isCode: false, str: str.substring(last, match.index) }; } @@ -833,7 +839,7 @@ export function* forAnsiStringParts(str: string) { */ export function removeAnsiEscapeCodes(str: string): string { if (str) { - str = str.replace(CSI_SEQUENCE, ''); + str = str.replace(CONTROL_SEQUENCES, ''); } return str; diff --git a/src/vs/base/node/powershell.ts b/src/vs/base/node/powershell.ts index 05d68e3cefd..63d230f56bc 100644 --- a/src/vs/base/node/powershell.ts +++ b/src/vs/base/node/powershell.ts @@ -226,6 +226,13 @@ function findPSCoreDotnetGlobalTool(): IPossiblePowerShellExe { return new PossiblePowerShellExe(dotnetGlobalToolExePath, '.NET Core PowerShell Global Tool'); } +function findPSCoreScoopInstallation(): IPossiblePowerShellExe { + const scoopAppsDir = path.join(os.homedir(), 'scoop', 'apps'); + const scoopPwsh = path.join(scoopAppsDir, 'pwsh', 'current', 'pwsh.exe'); + + return new PossiblePowerShellExe(scoopPwsh, 'PowerShell (Scoop)'); +} + function findWinPS(): IPossiblePowerShellExe | null { const winPSPath = path.join( process.env.windir!, @@ -285,6 +292,11 @@ async function* enumerateDefaultPowerShellInstallations(): AsyncIterable(this.port, 'message', (e: MessageEvent) => { - if (e.data) { - return VSBuffer.wrap(e.data); - } - return VSBuffer.alloc(0); - }); + readonly onMessage; constructor(private port: MessagePort) { - + this.onMessage = Event.fromDOMEventEmitter(this.port, 'message', (e: MessageEvent) => { + if (e.data) { + return VSBuffer.wrap(e.data); + } + return VSBuffer.alloc(0); + }); // we must call start() to ensure messages are flowing port.start(); } diff --git a/src/vs/base/parts/ipc/node/ipc.mp.ts b/src/vs/base/parts/ipc/node/ipc.mp.ts index 87e1016f045..df9cd52c58a 100644 --- a/src/vs/base/parts/ipc/node/ipc.mp.ts +++ b/src/vs/base/parts/ipc/node/ipc.mp.ts @@ -15,15 +15,15 @@ import { assertType } from '../../../common/types.js'; */ class Protocol implements IMessagePassingProtocol { - readonly onMessage = Event.fromNodeEventEmitter(this.port, 'message', (e: MessageEvent) => { - if (e.data) { - return VSBuffer.wrap(e.data); - } - return VSBuffer.alloc(0); - }); + readonly onMessage; constructor(private port: MessagePortMain) { - + this.onMessage = Event.fromNodeEventEmitter(this.port, 'message', (e: MessageEvent) => { + if (e.data) { + return VSBuffer.wrap(e.data); + } + return VSBuffer.alloc(0); + }); // we must call start() to ensure messages are flowing port.start(); } diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index 2def700f2b0..bf674966c0a 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -38,13 +38,20 @@ export class SQLiteStorageDatabase implements IStorageDatabase { private static readonly BUSY_OPEN_TIMEOUT = 2000; // timeout in ms to retry when opening DB fails with SQLITE_BUSY private static readonly MAX_HOST_PARAMETERS = 256; // maximum number of parameters within a statement - private readonly name = basename(this.path); + private readonly name: string; - private readonly logger = new SQLiteStorageDatabaseLogger(this.options.logging); + private readonly logger: SQLiteStorageDatabaseLogger; - private readonly whenConnected = this.connect(this.path); + private readonly whenConnected: Promise; - constructor(private readonly path: string, private readonly options: ISQLiteStorageDatabaseOptions = Object.create(null)) { } + constructor( + private readonly path: string, + options: ISQLiteStorageDatabaseOptions = Object.create(null) + ) { + this.name = basename(this.path); + this.logger = new SQLiteStorageDatabaseLogger(options.logging); + this.whenConnected = this.connect(this.path); + } async getItems(): Promise> { const connection = await this.whenConnected; diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index 3263774598e..dcda5f102e5 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -852,7 +852,7 @@ suite('MarkdownRenderer', () => { }); test('incomplete link target with incomplete arg 2', () => { - const incomplete = '[text](command:_github.copilot.openRelativePath "arg'; + const incomplete = '[text](command:vscode.openRelativePath "arg'; const tokens = marked.marked.lexer(incomplete); const newTokens = fillInIncompleteTokens(tokens); diff --git a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts index 4f7ba0d46c8..6b3d35df9ff 100644 --- a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts @@ -12,6 +12,7 @@ import { IAsyncDataSource, ITreeNode } from '../../../../browser/ui/tree/tree.js import { timeout } from '../../../../common/async.js'; import { Iterable } from '../../../../common/iterator.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../common/utils.js'; +import { runWithFakedTimers } from '../../../common/timeTravelScheduler.js'; interface Element { id: string; @@ -465,45 +466,47 @@ suite('AsyncDataTree', function () { }); test('issue #80098 - first expand should call getChildren', async () => { - const container = document.createElement('div'); + return runWithFakedTimers({ useFakeTimers: true }, async () => { + const container = document.createElement('div'); - const calls: Function[] = []; - const dataSource = new class implements IAsyncDataSource { - hasChildren(element: Element): boolean { - return !!element.children && element.children.length > 0; - } - getChildren(element: Element): Promise { - return new Promise(c => calls.push(() => c(element.children || []))); - } - }; + const calls: Function[] = []; + const dataSource = new class implements IAsyncDataSource { + hasChildren(element: Element): boolean { + return !!element.children && element.children.length > 0; + } + getChildren(element: Element): Promise { + return new Promise(c => calls.push(() => c(element.children || []))); + } + }; - const model = new Model({ - id: 'root', - children: [{ - id: 'a', children: [{ - id: 'aa' + const model = new Model({ + id: 'root', + children: [{ + id: 'a', children: [{ + id: 'aa' + }] }] - }] + }); + + const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() })); + tree.layout(200); + + const pSetInput = tree.setInput(model.root); + calls.pop()!(); // resolve getChildren(root) + await pSetInput; + + const pExpandA = tree.expand(model.get('a')); + assert.strictEqual(calls.length, 1, 'expand(a) should\'ve called getChildren(a)'); + + let race = await Promise.race([pExpandA.then(() => 'expand'), timeout(1).then(() => 'timeout')]); + assert.strictEqual(race, 'timeout', 'expand(a) should not be yet done'); + + calls.pop()!(); + assert.strictEqual(calls.length, 0, 'no pending getChildren calls'); + + race = await Promise.race([pExpandA.then(() => 'expand'), timeout(1).then(() => 'timeout')]); + assert.strictEqual(race, 'expand', 'expand(a) should now be done'); }); - - const tree = store.add(new AsyncDataTree('test', container, new VirtualDelegate(), [new Renderer()], dataSource, { identityProvider: new IdentityProvider() })); - tree.layout(200); - - const pSetInput = tree.setInput(model.root); - calls.pop()!(); // resolve getChildren(root) - await pSetInput; - - const pExpandA = tree.expand(model.get('a')); - assert.strictEqual(calls.length, 1, 'expand(a) should\'ve called getChildren(a)'); - - let race = await Promise.race([pExpandA.then(() => 'expand'), timeout(1).then(() => 'timeout')]); - assert.strictEqual(race, 'timeout', 'expand(a) should not be yet done'); - - calls.pop()!(); - assert.strictEqual(calls.length, 0, 'no pending getChildren calls'); - - race = await Promise.race([pExpandA.then(() => 'expand'), timeout(1).then(() => 'timeout')]); - assert.strictEqual(race, 'expand', 'expand(a) should now be done'); }); test('issue #78388 - tree should react to hasChildren toggles', async () => { diff --git a/src/vs/base/test/common/arrays.test.ts b/src/vs/base/test/common/arrays.test.ts index f1144fbb613..b9a18cffdf2 100644 --- a/src/vs/base/test/common/arrays.test.ts +++ b/src/vs/base/test/common/arrays.test.ts @@ -6,6 +6,7 @@ import assert from 'assert'; import * as arrays from '../../common/arrays.js'; import * as arraysFind from '../../common/arraysFind.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import { pick } from '../../common/arrays.js'; suite('Arrays', () => { @@ -399,6 +400,64 @@ suite('Arrays', () => { ); }); + suite('pick', () => { + suite('object', () => { + test('numbers', () => { + const array = [{ v: 3, foo: 'a' }, { v: 5, foo: 'b' }, { v: 2, foo: 'c' }, { v: 2, foo: 'd' }, { v: 17, bar: '1' }, { v: -100, baz: '10' }]; + + assert.deepStrictEqual( + array.map(pick('v')), + [3, 5, 2, 2, 17, -100], + ); + }); + + test('strings', () => { + const array = [{ v: 3, foo: 'a' }, { v: 5, foo: 'b' }, { v: 2, foo: 'c' }, { v: 2, foo: 'd' }, { v: 17, bar: '1' }, { v: -100, baz: '10' }, { foo: '12' }]; + + assert.deepStrictEqual( + array.map(pick('foo')), + ['a', 'b', 'c', 'd', undefined, undefined, '12'], + ); + }); + + test('booleans', () => { + const array = [{ v: 3, foo: 'a' }, { v: 5, foo: 'b' }, { v: 2, foo: 'c' }, { v: 2, foo: 'd' }, { v: 17, bar: true }, { v: -100, bar: false }, { bar: false }]; + + assert.deepStrictEqual( + array.map(pick('bar')), + [undefined, undefined, undefined, undefined, true, false, false], + ); + }); + + test('objects', () => { + const array = [{ v: { test: 12 } }, { v: { test: 24 } }, {}, { v: { test: 17892 } }]; + + assert.deepStrictEqual( + array.map(pick('v')), + [{ test: 12 }, { test: 24 }, undefined, { test: 17892 }], + ); + }); + + test('mixed', () => { + const array = [{ v: { test: 104 } }, { v: 2 }, {}, { v: '24' }, { v: null }]; + + assert.deepStrictEqual( + array.map(pick('v')), + [{ test: 104 }, 2, undefined, '24', null], + ); + }); + }); + + test('string', () => { + const array = ['haallo', 'there', ':wave:', '!']; + + assert.deepStrictEqual( + array.map(pick('length')), + [6, 5, 6, 1], + ); + }); + }); + suite('ArrayQueue', () => { suite('takeWhile/takeFromEndWhile', () => { test('TakeWhile 1', () => { diff --git a/src/vs/base/test/common/date.test.ts b/src/vs/base/test/common/date.test.ts index 2260d6104b5..287d192cea5 100644 --- a/src/vs/base/test/common/date.test.ts +++ b/src/vs/base/test/common/date.test.ts @@ -37,11 +37,13 @@ suite('Date', () => { test('yesterday', () => { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(12); strictEqual(fromNowByDay(yesterday), 'Yesterday'); }); test('daysAgo', () => { const daysAgo = new Date(); daysAgo.setDate(daysAgo.getDate() - 5); + daysAgo.setHours(daysAgo.getHours() - 2); // 2 hours further to avoid DST issues strictEqual(fromNowByDay(daysAgo, true), '5 days ago'); }); }); diff --git a/src/vs/base/test/common/lifecycle.test.ts b/src/vs/base/test/common/lifecycle.test.ts index fd2fa4ef562..6576cab31bd 100644 --- a/src/vs/base/test/common/lifecycle.test.ts +++ b/src/vs/base/test/common/lifecycle.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { Emitter } from '../../common/event.js'; -import { DisposableStore, dispose, IDisposable, markAsSingleton, ReferenceCollection, SafeDisposable, toDisposable } from '../../common/lifecycle.js'; +import { DisposableStore, dispose, IDisposable, markAsSingleton, ReferenceCollection, SafeDisposable, thenIfNotDisposed, toDisposable } from '../../common/lifecycle.js'; import { ensureNoDisposablesAreLeakedInTestSuite, throwIfDisposablesAreLeaked } from './utils.js'; class Disposable implements IDisposable { @@ -328,4 +328,30 @@ suite('No Leakage Utilities', () => { toDisposable(() => { }).dispose(); }); }); + + suite('thenIfNotDisposed', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('normal case', async () => { + let called = false; + store.add(thenIfNotDisposed(Promise.resolve(123), (result: number) => { + assert.strictEqual(result, 123); + called = true; + })); + + await new Promise(resolve => setTimeout(resolve, 0)); + assert.strictEqual(called, true); + }); + + test('disposed before promise resolves', async () => { + let called = false; + const disposable = thenIfNotDisposed(Promise.resolve(123), () => { + called = true; + }); + + disposable.dispose(); + await new Promise(resolve => setTimeout(resolve, 0)); + assert.strictEqual(called, false); + }); + }); }); diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index 7c567df4b6f..dca2a40410e 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -422,132 +422,9 @@ suite('Strings', () => { }), 'a0ca1ca2ca3c'); }); - test('removeAnsiEscapeCodes', () => { - const CSI = '\x1b\['; - const sequences = [ - // Base cases from https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ - `${CSI}42@`, - `${CSI}42 @`, - `${CSI}42A`, - `${CSI}42 A`, - `${CSI}42B`, - `${CSI}42C`, - `${CSI}42D`, - `${CSI}42E`, - `${CSI}42F`, - `${CSI}42G`, - `${CSI}42;42H`, - `${CSI}42I`, - `${CSI}42J`, - `${CSI}?42J`, - `${CSI}42K`, - `${CSI}?42K`, - `${CSI}42L`, - `${CSI}42M`, - `${CSI}42P`, - `${CSI}#P`, - `${CSI}3#P`, - `${CSI}#Q`, - `${CSI}3#Q`, - `${CSI}#R`, - `${CSI}42S`, - `${CSI}?1;2;3S`, - `${CSI}42T`, - `${CSI}42;42;42;42;42T`, - `${CSI}>3T`, - `${CSI}42X`, - `${CSI}42Z`, - `${CSI}42^`, - `${CSI}42\``, - `${CSI}42a`, - `${CSI}42b`, - `${CSI}42c`, - `${CSI}=42c`, - `${CSI}>42c`, - `${CSI}42d`, - `${CSI}42e`, - `${CSI}42;42f`, - `${CSI}42g`, - `${CSI}3h`, - `${CSI}?3h`, - `${CSI}42i`, - `${CSI}?42i`, - `${CSI}3l`, - `${CSI}?3l`, - `${CSI}3m`, - `${CSI}>0;0m`, - `${CSI}>0m`, - `${CSI}?0m`, - `${CSI}42n`, - `${CSI}>42n`, - `${CSI}?42n`, - `${CSI}>42p`, - `${CSI}!p`, - `${CSI}0;0"p`, - `${CSI}42$p`, - `${CSI}?42$p`, - `${CSI}#p`, - `${CSI}3#p`, - `${CSI}>42q`, - `${CSI}42q`, - `${CSI}42 q`, - `${CSI}42"q`, - `${CSI}#q`, - `${CSI}42;42r`, - `${CSI}?3r`, - `${CSI}0;0;0;0;3$r`, - `${CSI}s`, - `${CSI}0;0s`, - `${CSI}>42s`, - `${CSI}?3s`, - `${CSI}42;42;42t`, - `${CSI}>3t`, - `${CSI}42 t`, - `${CSI}0;0;0;0;3$t`, - `${CSI}u`, - `${CSI}42 u`, - `${CSI}0;0;0;0;0;0;0;0$v`, - `${CSI}42$w`, - `${CSI}0;0;0;0'w`, - `${CSI}42x`, - `${CSI}42*x`, - `${CSI}0;0;0;0;0$x`, - `${CSI}42#y`, - `${CSI}0;0;0;0;0;0*y`, - `${CSI}42;0'z`, - `${CSI}0;1;2;4$z`, - `${CSI}3'{`, - `${CSI}#{`, - `${CSI}3#{`, - `${CSI}0;0;0;0\${`, - `${CSI}0;0;0;0#|`, - `${CSI}42$|`, - `${CSI}42'|`, - `${CSI}42*|`, - `${CSI}#}`, - `${CSI}42'}`, - `${CSI}42$}`, - `${CSI}42'~`, - `${CSI}42$~`, - - // Common SGR cases: - `${CSI}1;31m`, // multiple attrs - `${CSI}105m`, // bright background - `${CSI}48:5:128m`, // 256 indexed color - `${CSI}48;5;128m`, // 256 indexed color alt - `${CSI}38:2:0:255:255:255m`, // truecolor - `${CSI}38;2;255;255;255m`, // truecolor alt - - // Custom sequences: - '\x1b]633;SetMark;\x07', - '\x1b]633;P;Cwd=/foo\x07', - ]; - - for (const sequence of sequences) { + suite('removeAnsiEscapeCodes', () => { + function testSequence(sequence: string) { assert.strictEqual(strings.removeAnsiEscapeCodes(`hello${sequence}world`), 'helloworld', `expect to remove ${JSON.stringify(sequence)}`); - } - - for (const sequence of sequences) { assert.deepStrictEqual( [...strings.forAnsiStringParts(`hello${sequence}world`)], [{ isCode: false, str: 'hello' }, { isCode: true, str: sequence }, { isCode: false, str: 'world' }], @@ -555,10 +432,203 @@ suite('Strings', () => { ); } - // #209937 - assert.strictEqual( - strings.removeAnsiEscapeCodes(`localhost:\x1b[31m1234`), - 'localhost:1234',); + test('CSI sequences', () => { + const CSI = '\x1b['; + const sequences = [ + // Base cases from https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ + `${CSI}42@`, + `${CSI}42 @`, + `${CSI}42A`, + `${CSI}42 A`, + `${CSI}42B`, + `${CSI}42C`, + `${CSI}42D`, + `${CSI}42E`, + `${CSI}42F`, + `${CSI}42G`, + `${CSI}42;42H`, + `${CSI}42I`, + `${CSI}42J`, + `${CSI}?42J`, + `${CSI}42K`, + `${CSI}?42K`, + `${CSI}42L`, + `${CSI}42M`, + `${CSI}42P`, + `${CSI}#P`, + `${CSI}3#P`, + `${CSI}#Q`, + `${CSI}3#Q`, + `${CSI}#R`, + `${CSI}42S`, + `${CSI}?1;2;3S`, + `${CSI}42T`, + `${CSI}42;42;42;42;42T`, + `${CSI}>3T`, + `${CSI}42X`, + `${CSI}42Z`, + `${CSI}42^`, + `${CSI}42\``, + `${CSI}42a`, + `${CSI}42b`, + `${CSI}42c`, + `${CSI}=42c`, + `${CSI}>42c`, + `${CSI}42d`, + `${CSI}42e`, + `${CSI}42;42f`, + `${CSI}42g`, + `${CSI}3h`, + `${CSI}?3h`, + `${CSI}42i`, + `${CSI}?42i`, + `${CSI}3l`, + `${CSI}?3l`, + `${CSI}3m`, + `${CSI}>0;0m`, + `${CSI}>0m`, + `${CSI}?0m`, + `${CSI}42n`, + `${CSI}>42n`, + `${CSI}?42n`, + `${CSI}>42p`, + `${CSI}!p`, + `${CSI}0;0"p`, + `${CSI}42$p`, + `${CSI}?42$p`, + `${CSI}#p`, + `${CSI}3#p`, + `${CSI}>42q`, + `${CSI}42q`, + `${CSI}42 q`, + `${CSI}42"q`, + `${CSI}#q`, + `${CSI}42;42r`, + `${CSI}?3r`, + `${CSI}0;0;0;0;3$r`, + `${CSI}s`, + `${CSI}0;0s`, + `${CSI}>42s`, + `${CSI}?3s`, + `${CSI}42;42;42t`, + `${CSI}>3t`, + `${CSI}42 t`, + `${CSI}0;0;0;0;3$t`, + `${CSI}u`, + `${CSI}42 u`, + `${CSI}0;0;0;0;0;0;0;0$v`, + `${CSI}42$w`, + `${CSI}0;0;0;0'w`, + `${CSI}42x`, + `${CSI}42*x`, + `${CSI}0;0;0;0;0$x`, + `${CSI}42#y`, + `${CSI}0;0;0;0;0;0*y`, + `${CSI}42;0'z`, + `${CSI}0;1;2;4$z`, + `${CSI}3'{`, + `${CSI}#{`, + `${CSI}3#{`, + `${CSI}0;0;0;0\${`, + `${CSI}0;0;0;0#|`, + `${CSI}42$|`, + `${CSI}42'|`, + `${CSI}42*|`, + `${CSI}#}`, + `${CSI}42'}`, + `${CSI}42$}`, + `${CSI}42'~`, + `${CSI}42$~`, + + // Common SGR cases: + `${CSI}1;31m`, // multiple attrs + `${CSI}105m`, // bright background + `${CSI}48:5:128m`, // 256 indexed color + `${CSI}48;5;128m`, // 256 indexed color alt + `${CSI}38:2:0:255:255:255m`, // truecolor + `${CSI}38;2;255;255;255m`, // truecolor alt + ]; + + for (const sequence of sequences) { + testSequence(sequence); + } + }); + + suite('OSC sequences', () => { + function testOscSequence(prefix: string, suffix: string) { + const sequenceContent = [ + `633;SetMark;`, + `633;P;Cwd=/foo`, + `7;file://local/Users/me/foo/bar` + ]; + + const sequences = []; + for (const content of sequenceContent) { + sequences.push(`${prefix}${content}${suffix}`); + } + for (const sequence of sequences) { + testSequence(sequence); + } + } + test('ESC ] Ps ; Pt ESC \\', () => { + testOscSequence('\x1b]', '\x1b\\'); + }); + test('ESC ] Ps ; Pt BEL', () => { + testOscSequence('\x1b]', '\x07'); + }); + test('ESC ] Ps ; Pt ST', () => { + testOscSequence('\x1b]', '\x9c'); + }); + test('OSC Ps ; Pt ESC \\', () => { + testOscSequence('\x9d', '\x1b\\'); + }); + test('OSC Ps ; Pt BEL', () => { + testOscSequence('\x9d', '\x07'); + }); + test('OSC Ps ; Pt ST', () => { + testOscSequence('\x9d', '\x9c'); + }); + }); + + test('ESC sequences', () => { + const sequenceContent = [ + ` F`, + ` G`, + ` L`, + ` M`, + ` N`, + `#3`, + `#4`, + `#5`, + `#6`, + `#8`, + `%@`, + `%G`, + `(C`, + `)C`, + `*C`, + `+C`, + `-C`, + `.C`, + `/C` + ]; + const sequences = []; + for (const content of sequenceContent) { + sequences.push(`\x1b${content}`); + } + for (const sequence of sequences) { + testSequence(sequence); + } + }); + + suite('regression tests', () => { + test('#209937', () => { + assert.strictEqual( + strings.removeAnsiEscapeCodes(`localhost:\x1b[31m1234`), + 'localhost:1234' + ); + }); + }); }); test('removeAnsiEscapeCodesFromPrompt', () => { diff --git a/src/vs/base/worker/workerMain.ts b/src/vs/base/worker/workerMain.ts index a094636cc84..bd1880e77a7 100644 --- a/src/vs/base/worker/workerMain.ts +++ b/src/vs/base/worker/workerMain.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// TODO @hediet @alexdima check where this code is used or remove this file +// (code oss runs fine without this file, but is probably needed by the monaco-editor). + (function () { function loadCode(moduleId: string): Promise { diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index faa711a30c5..ab9671491fb 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -28,6 +28,7 @@ interface ISecretStorageCrypto { } class TransparentCrypto implements ISecretStorageCrypto { + async seal(data: string): Promise { return data; } @@ -44,6 +45,7 @@ const enum AESConstants { } class NetworkError extends Error { + constructor(inner: Error) { super(inner.message); this.name = inner.name; @@ -52,10 +54,13 @@ class NetworkError extends Error { } class ServerKeyedAESCrypto implements ISecretStorageCrypto { - private _serverKey: Uint8Array | undefined; - /** Gets whether the algorithm is supported; requires a secure context */ - public static supported() { + private serverKey: Uint8Array | undefined; + + /** + * Gets whether the algorithm is supported; requires a secure context + */ + static supported() { return !!crypto.subtle; } @@ -141,8 +146,8 @@ class ServerKeyedAESCrypto implements ISecretStorageCrypto { } private async getServerKeyPart(): Promise { - if (this._serverKey) { - return this._serverKey; + if (this.serverKey) { + return this.serverKey; } let attempt = 0; @@ -154,12 +159,15 @@ class ServerKeyedAESCrypto implements ISecretStorageCrypto { if (!res.ok) { throw new Error(res.statusText); } + const serverKey = new Uint8Array(await res.arrayBuffer()); if (serverKey.byteLength !== AESConstants.KEY_LENGTH / 8) { throw Error(`The key retrieved by the server is not ${AESConstants.KEY_LENGTH} bit long.`); } - this._serverKey = serverKey; - return this._serverKey; + + this.serverKey = serverKey; + + return this.serverKey; } catch (e) { lastError = e instanceof Error ? e : new Error(String(e)); attempt++; @@ -172,34 +180,39 @@ class ServerKeyedAESCrypto implements ISecretStorageCrypto { if (lastError) { throw new NetworkError(lastError); } + throw new Error('Unknown error'); } } export class LocalStorageSecretStorageProvider implements ISecretStorageProvider { - private readonly _storageKey = 'secrets.provider'; - private _secretsPromise: Promise> = this.load(); + private readonly storageKey = 'secrets.provider'; + + private secretsPromise: Promise>; type: 'in-memory' | 'persisted' | 'unknown' = 'persisted'; constructor( private readonly crypto: ISecretStorageCrypto, - ) { } + ) { + this.secretsPromise = this.load(); + } private async load(): Promise> { const record = this.loadAuthSessionFromElement(); - // Get the secrets from localStorage - const encrypted = localStorage.getItem(this._storageKey); + + const encrypted = localStorage.getItem(this.storageKey); if (encrypted) { try { const decrypted = JSON.parse(await this.crypto.unseal(encrypted)); + return { ...record, ...decrypted }; } catch (err) { // TODO: send telemetry console.error('Failed to decrypt secrets from localStorage', err); if (!(err instanceof NetworkError)) { - localStorage.removeItem(this._storageKey); + localStorage.removeItem(this.storageKey); } } } @@ -243,33 +256,35 @@ export class LocalStorageSecretStorageProvider implements ISecretStorageProvider } async get(key: string): Promise { - const secrets = await this._secretsPromise; + const secrets = await this.secretsPromise; + return secrets[key]; } + async set(key: string, value: string): Promise { - const secrets = await this._secretsPromise; + const secrets = await this.secretsPromise; secrets[key] = value; - this._secretsPromise = Promise.resolve(secrets); + this.secretsPromise = Promise.resolve(secrets); this.save(); } + async delete(key: string): Promise { - const secrets = await this._secretsPromise; + const secrets = await this.secretsPromise; delete secrets[key]; - this._secretsPromise = Promise.resolve(secrets); + this.secretsPromise = Promise.resolve(secrets); this.save(); } private async save(): Promise { try { - const encrypted = await this.crypto.seal(JSON.stringify(await this._secretsPromise)); - localStorage.setItem(this._storageKey, encrypted); + const encrypted = await this.crypto.seal(JSON.stringify(await this.secretsPromise)); + localStorage.setItem(this.storageKey, encrypted); } catch (err) { console.error(err); } } } - class LocalStorageURLCallbackProvider extends Disposable implements IURLCallbackProvider { private static REQUEST_ID = 0; @@ -485,6 +500,7 @@ class WorkspaceProvider implements IWorkspaceProvider { return !!result; } } + return false; } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 21a297a12d8..bc7834b4c41 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -118,6 +118,10 @@ import { IAuxiliaryWindowsMainService } from '../../platform/auxiliaryWindow/ele import { AuxiliaryWindowsMainService } from '../../platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.js'; import { normalizeNFC } from '../../base/common/normalization.js'; import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js'; +import { INativeMcpDiscoveryHelperService, NativeMcpDiscoveryHelperChannelName } from '../../platform/mcp/common/nativeMcpDiscoveryHelper.js'; +import { NativeMcpDiscoveryHelperService } from '../../platform/mcp/node/nativeMcpDiscoveryHelperService.js'; +import { IWebContentExtractorService } from '../../platform/webContentExtractor/common/webContentExtractor.js'; +import { NativeWebContentExtractorService } from '../../platform/webContentExtractor/electron-main/webContentExtractorService.js'; /** * The main VS Code application. There will only ever be one instance, @@ -1046,6 +1050,9 @@ export class CodeApplication extends Disposable { // Native Host services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService, undefined, false /* proxied to other processes */)); + // Web Contents Extractor + services.set(IWebContentExtractorService, new SyncDescriptor(NativeWebContentExtractorService, undefined, false /* proxied to other processes */)); + // Webview Manager services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService)); @@ -1119,6 +1126,10 @@ export class CodeApplication extends Disposable { // Proxy Auth services.set(IProxyAuthService, new SyncDescriptor(ProxyAuthService)); + // MCP + services.set(INativeMcpDiscoveryHelperService, new SyncDescriptor(NativeMcpDiscoveryHelperService)); + + // Dev Only: CSS service (for ESM) services.set(ICSSDevelopmentService, new SyncDescriptor(CSSDevelopmentService, undefined, true)); @@ -1189,6 +1200,10 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('nativeHost', nativeHostChannel); sharedProcessClient.then(client => client.registerChannel('nativeHost', nativeHostChannel)); + // Web Content Extractor + const webContentExtractorChannel = ProxyChannel.fromService(accessor.get(IWebContentExtractorService), disposables); + mainProcessElectronServer.registerChannel('webContentExtractor', webContentExtractorChannel); + // Workspaces const workspacesChannel = ProxyChannel.fromService(accessor.get(IWorkspacesService), disposables); mainProcessElectronServer.registerChannel('workspaces', workspacesChannel); @@ -1222,6 +1237,10 @@ export class CodeApplication extends Disposable { const externalTerminalChannel = ProxyChannel.fromService(accessor.get(IExternalTerminalMainService), disposables); mainProcessElectronServer.registerChannel('externalTerminal', externalTerminalChannel); + // MCP + const mcpDiscoveryChannel = ProxyChannel.fromService(accessor.get(INativeMcpDiscoveryHelperService), disposables); + mainProcessElectronServer.registerChannel(NativeMcpDiscoveryHelperChannelName, mcpDiscoveryChannel); + // Logger const loggerChannel = new LoggerChannel(accessor.get(ILoggerMainService),); mainProcessElectronServer.registerChannel('logger', loggerChannel); diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 3f819e1662b..84c4e12898f 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -198,9 +198,16 @@ class CodeMain { fileService.registerProvider(Schemas.vscodeUserData, new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.vscodeUserData, userDataProfilesMainService, uriIdentityService, logService)); // Policy - const policyService = isWindows && productService.win32RegValueName ? disposables.add(new NativePolicyService(logService, productService.win32RegValueName)) - : environmentMainService.policyFile ? disposables.add(new FilePolicyService(environmentMainService.policyFile, fileService, logService)) - : new NullPolicyService(); + let policyService: IPolicyService | undefined; + if (isWindows && productService.win32RegValueName) { + policyService = disposables.add(new NativePolicyService(logService, productService.win32RegValueName)); + } else if (isMacintosh && productService.darwinBundleIdentifier) { + policyService = disposables.add(new NativePolicyService(logService, productService.darwinBundleIdentifier)); + } else if (environmentMainService.policyFile) { + policyService = disposables.add(new FilePolicyService(environmentMainService.policyFile, fileService, logService)); + } else { + policyService = new NullPolicyService(); + } services.set(IPolicyService, policyService); // Configuration diff --git a/src/vs/code/electron-sandbox/workbench/workbench.ts b/src/vs/code/electron-sandbox/workbench/workbench.ts index 47c3d28a6fc..44a6051573c 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.ts +++ b/src/vs/code/electron-sandbox/workbench/workbench.ts @@ -90,15 +90,19 @@ splash.className = baseTheme ?? 'vs-dark'; if (layoutInfo.windowBorder && colorInfo.windowBorder) { - splash.style.position = 'relative'; - splash.style.height = 'calc(100vh - 2px)'; - splash.style.width = 'calc(100vw - 2px)'; - splash.style.border = `1px solid var(--window-border-color)`; - splash.style.setProperty('--window-border-color', colorInfo.windowBorder); + const borderElement = document.createElement('div'); + borderElement.style.position = 'absolute'; + borderElement.style.width = 'calc(100vw - 2px)'; + borderElement.style.height = 'calc(100vh - 2px)'; + borderElement.style.zIndex = '1'; // allow border above other elements + borderElement.style.border = `1px solid var(--window-border-color)`; + borderElement.style.setProperty('--window-border-color', colorInfo.windowBorder); if (layoutInfo.windowBorderRadius) { - splash.style.borderRadius = layoutInfo.windowBorderRadius; + borderElement.style.borderRadius = layoutInfo.windowBorderRadius; } + + splash.appendChild(borderElement); } // ensure there is enough space diff --git a/src/vs/code/electron-utility/sharedProcess/contrib/codeCacheCleaner.ts b/src/vs/code/electron-utility/sharedProcess/contrib/codeCacheCleaner.ts index 7560b8fac32..5e7ebeaee5d 100644 --- a/src/vs/code/electron-utility/sharedProcess/contrib/codeCacheCleaner.ts +++ b/src/vs/code/electron-utility/sharedProcess/contrib/codeCacheCleaner.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; +import { promises } from 'fs'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; @@ -14,17 +14,19 @@ import { IProductService } from '../../../../platform/product/common/productServ export class CodeCacheCleaner extends Disposable { - private readonly _DataMaxAge = this.productService.quality !== 'stable' - ? 1000 * 60 * 60 * 24 * 7 // roughly 1 week (insiders) - : 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months (stable) + private readonly dataMaxAge: number; constructor( currentCodeCachePath: string | undefined, - @IProductService private readonly productService: IProductService, + @IProductService productService: IProductService, @ILogService private readonly logService: ILogService ) { super(); + this.dataMaxAge = productService.quality !== 'stable' + ? 1000 * 60 * 60 * 24 * 7 // roughly 1 week (insiders) + : 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months (stable) + // Cached data is stored as user data and we run a cleanup task every time // the editor starts. The strategy is to delete all files that are older than // 3 months (1 week respectively) @@ -55,8 +57,8 @@ export class CodeCacheCleaner extends Disposable { // Delete cache folder if old enough const codeCacheEntryPath = join(codeCacheRootPath, codeCache); - const codeCacheEntryStat = await fs.promises.stat(codeCacheEntryPath); - if (codeCacheEntryStat.isDirectory() && (now - codeCacheEntryStat.mtime.getTime()) > this._DataMaxAge) { + const codeCacheEntryStat = await promises.stat(codeCacheEntryPath); + if (codeCacheEntryStat.isDirectory() && (now - codeCacheEntryStat.mtime.getTime()) > this.dataMaxAge) { this.logService.trace(`[code cache cleanup]: Removing code cache folder ${codeCache}.`); return Promises.rm(codeCacheEntryPath); diff --git a/src/vs/code/electron-utility/sharedProcess/contrib/languagePackCachedDataCleaner.ts b/src/vs/code/electron-utility/sharedProcess/contrib/languagePackCachedDataCleaner.ts index 0c70b33f15c..f3f351ebaa4 100644 --- a/src/vs/code/electron-utility/sharedProcess/contrib/languagePackCachedDataCleaner.ts +++ b/src/vs/code/electron-utility/sharedProcess/contrib/languagePackCachedDataCleaner.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; +import { promises } from 'fs'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; @@ -33,17 +33,19 @@ interface ILanguagePackFile { export class LanguagePackCachedDataCleaner extends Disposable { - private readonly _DataMaxAge = this.productService.quality !== 'stable' - ? 1000 * 60 * 60 * 24 * 7 // roughly 1 week (insiders) - : 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months (stable) + private readonly dataMaxAge: number; constructor( @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, @ILogService private readonly logService: ILogService, - @IProductService private readonly productService: IProductService + @IProductService productService: IProductService ) { super(); + this.dataMaxAge = productService.quality !== 'stable' + ? 1000 * 60 * 60 * 24 * 7 // roughly 1 week (insiders) + : 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months (stable) + // We have no Language pack support for dev version (run from source) // So only cleanup when we have a build version. if (this.environmentService.isBuilt) { @@ -59,7 +61,7 @@ export class LanguagePackCachedDataCleaner extends Disposable { try { const installed: IStringDictionary = Object.create(null); - const metaData: ILanguagePackFile = JSON.parse(await fs.promises.readFile(join(this.environmentService.userDataPath, 'languagepacks.json'), 'utf8')); + const metaData: ILanguagePackFile = JSON.parse(await promises.readFile(join(this.environmentService.userDataPath, 'languagepacks.json'), 'utf8')); for (const locale of Object.keys(metaData)) { const entry = metaData[locale]; installed[`${entry.hash}.${locale}`] = true; @@ -94,8 +96,8 @@ export class LanguagePackCachedDataCleaner extends Disposable { } const candidate = join(folder, entry); - const stat = await fs.promises.stat(candidate); - if (stat.isDirectory() && (now - stat.mtime.getTime()) > this._DataMaxAge) { + const stat = await promises.stat(candidate); + if (stat.isDirectory() && (now - stat.mtime.getTime()) > this.dataMaxAge) { this.logService.trace(`[language pack cache cleanup]: Removing language pack cache folder: ${join(packEntry, entry)}`); await Promises.rm(candidate); diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index 16e9793483b..e0240babbcf 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -120,6 +120,8 @@ import { getCodeDisplayProtocol, getDisplayProtocol } from '../../../base/node/o import { RequestService } from '../../../platform/request/electron-utility/requestService.js'; import { DefaultExtensionsInitializer } from './contrib/defaultExtensionsInitializer.js'; import { AllowedExtensionsService } from '../../../platform/extensionManagement/common/allowedExtensionsService.js'; +import { IExtensionGalleryManifestService } from '../../../platform/extensionManagement/common/extensionGalleryManifest.js'; +import { ExtensionGalleryManifestIPCService } from '../../../platform/extensionManagement/common/extensionGalleryManifestServiceIpc.js'; class SharedProcessMain extends Disposable implements IClientConnectionFilter { @@ -331,6 +333,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true)); // Extension Gallery + services.set(IExtensionGalleryManifestService, new ExtensionGalleryManifestIPCService(this.server, productService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService, undefined, true)); // Extension Tips @@ -376,8 +379,6 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { private initChannels(accessor: ServicesAccessor): void { - // const disposables = this._register(new DisposableStore()); - // Extensions Management const channel = new ExtensionManagementChannel(accessor.get(IExtensionManagementService), () => null); this.server.registerChannel('extensions', channel); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index e91bea07d6a..312df0c1f92 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -36,6 +36,7 @@ function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean { || !!argv['uninstall-extension'] || !!argv['update-extensions'] || !!argv['locate-extension'] + || !!argv['add-mcp'] || !!argv['telemetry']; } diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index aff3b2da579..7087d2ac431 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -65,6 +65,9 @@ import { localize } from '../../nls.js'; import { FileUserDataProvider } from '../../platform/userData/common/fileUserDataProvider.js'; import { addUNCHostToAllowlist, getUNCHost } from '../../base/node/unc.js'; import { AllowedExtensionsService } from '../../platform/extensionManagement/common/allowedExtensionsService.js'; +import { McpManagementCli } from '../../platform/mcp/common/mcpManagementCli.js'; +import { IExtensionGalleryManifestService } from '../../platform/extensionManagement/common/extensionGalleryManifest.js'; +import { ExtensionGalleryManifestService } from '../../platform/extensionManagement/common/extensionGalleryManifestService.js'; class CliMain extends Disposable { @@ -208,6 +211,7 @@ class CliMain extends Disposable { services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService, undefined, true)); services.set(IAllowedExtensionsService, new SyncDescriptor(AllowedExtensionsService, undefined, true)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true)); + services.set(IExtensionGalleryManifestService, new SyncDescriptor(ExtensionGalleryManifestService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService, undefined, true)); // Localizations @@ -304,6 +308,11 @@ class CliMain extends Disposable { return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).locateExtension(this.argv['locate-extension']); } + // Install MCP server + else if (this.argv['add-mcp']) { + return instantiationService.createInstance(McpManagementCli, new ConsoleLogger(LogLevel.Info, false)).addMcpDefinitions(this.argv['add-mcp']); + } + // Telemetry else if (this.argv['telemetry']) { console.log(await buildTelemetryMessage(environmentService.appRoot, environmentService.extensionsPath)); diff --git a/src/vs/editor/browser/config/migrateOptions.ts b/src/vs/editor/browser/config/migrateOptions.ts index 4adf4e3d848..a056495bfef 100644 --- a/src/vs/editor/browser/config/migrateOptions.ts +++ b/src/vs/editor/browser/config/migrateOptions.ts @@ -232,3 +232,10 @@ registerEditorSettingMigration('lightbulb.enabled', (value, read, write) => { } }); +// NES Code Shifting +registerEditorSettingMigration('inlineSuggest.edits.codeShifting', (value, read, write) => { + if (typeof value === 'boolean') { + write('inlineSuggest.edits.codeShifting', undefined); + write('inlineSuggest.edits.allowCodeShifting', value ? 'always' : 'never'); + } +}); diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css index 792c15feec2..5ca36c59620 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.css @@ -7,10 +7,10 @@ margin: 0; padding: 0; position: absolute; - overflow: hidden; + overflow-y: scroll; + scrollbar-width: none; z-index: -10; white-space: pre-wrap; - text-wrap: nowrap; } .monaco-editor .native-edit-context-textarea { diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 75acb7c2c8d..a6f5d41ad09 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -77,6 +77,10 @@ export class NativeEditContext extends AbstractEditContext { this.textArea = new FastDomNode(document.createElement('textarea')); this.textArea.setClassName('native-edit-context-textarea'); this.textArea.setAttribute('tabindex', '-1'); + this.domNode.setAttribute('autocorrect', 'off'); + this.domNode.setAttribute('autocapitalize', 'off'); + this.domNode.setAttribute('autocomplete', 'off'); + this.domNode.setAttribute('spellcheck', 'false'); this._updateDomAttributes(); @@ -208,6 +212,7 @@ export class NativeEditContext extends AbstractEditContext { public override onCursorStateChanged(e: ViewCursorStateChangedEvent): boolean { this._primarySelection = e.modelSelections[0] ?? new Selection(1, 1, 1, 1); this._screenReaderSupport.onCursorStateChanged(e); + this._updateEditContext(); return true; } diff --git a/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts b/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts index 769bb020d72..93a03823220 100644 --- a/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts +++ b/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts @@ -19,7 +19,7 @@ import { ViewContext } from '../../../../common/viewModel/viewContext.js'; import { applyFontInfo } from '../../../config/domFontInfo.js'; import { IEditorAriaOptions } from '../../../editorBrowser.js'; import { RestrictedRenderingContext, RenderingContext, HorizontalPosition } from '../../../view/renderingContext.js'; -import { ariaLabelForScreenReaderContent, ISimpleModel, newlinecount, PagedScreenReaderStrategy, ScreenReaderContentState } from '../screenReaderUtils.js'; +import { ariaLabelForScreenReaderContent, ISimpleModel, PagedScreenReaderStrategy, ScreenReaderContentState } from '../screenReaderUtils.js'; export class ScreenReaderSupport { @@ -27,6 +27,7 @@ export class ScreenReaderSupport { private _contentLeft: number = 1; private _contentWidth: number = 1; private _contentHeight: number = 1; + private _divWidth: number = 1; private _lineHeight: number = 1; private _fontInfo!: FontInfo; private _accessibilityPageSize: number = 1; @@ -69,12 +70,14 @@ export class ScreenReaderSupport { private _updateConfigurationSettings(): void { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); + const wrappingColumn = layoutInfo.wrappingColumn; this._contentLeft = layoutInfo.contentLeft; this._contentWidth = layoutInfo.contentWidth; this._contentHeight = layoutInfo.height; this._fontInfo = options.get(EditorOption.fontInfo); this._lineHeight = options.get(EditorOption.lineHeight); this._accessibilityPageSize = options.get(EditorOption.accessibilityPageSize); + this._divWidth = Math.round(wrappingColumn * this._fontInfo.typicalHalfwidthCharacterWidth); } private _updateDomAttributes(): void { @@ -88,6 +91,9 @@ export class ScreenReaderSupport { const tabSize = this._context.viewModel.model.getOptions().tabSize; const spaceWidth = options.get(EditorOption.fontInfo).spaceWidth; this._domNode.domNode.style.tabSize = `${tabSize * spaceWidth}px`; + const wordWrapOverride2 = options.get(EditorOption.wordWrapOverride2); + const wordWrapValue = wordWrapOverride2 !== 'inherit' ? wordWrapOverride2 : options.get(EditorOption.wordWrap); + this._domNode.domNode.style.textWrap = wordWrapValue === 'off' ? 'nowrap' : 'wrap'; } public onCursorStateChanged(e: ViewCursorStateChangedEvent): void { @@ -119,22 +125,25 @@ export class ScreenReaderSupport { } const editorScrollTop = this._context.viewLayout.getCurrentScrollTop(); - const top = this._context.viewLayout.getVerticalOffsetForLineNumber(this._primarySelection.positionLineNumber) - editorScrollTop; + const positionLineNumber = this._primarySelection.positionLineNumber; + const top = this._context.viewLayout.getVerticalOffsetForLineNumber(positionLineNumber) - editorScrollTop; if (top < 0 || top > this._contentHeight) { // cursor is outside the viewport this._renderAtTopLeft(); return; } - this._doRender(top, this._contentLeft, this._contentWidth, this._lineHeight); - this._setScrollTop(); + const offsetForStartPositionWithinEditor = this._context.viewLayout.getVerticalOffsetForLineNumber(this._screenReaderContentState.startPositionWithinEditor.lineNumber); + const offsetForPositionLineNumber = this._context.viewLayout.getVerticalOffsetForLineNumber(positionLineNumber); + const scrollTop = offsetForPositionLineNumber - offsetForStartPositionWithinEditor; + this._doRender(scrollTop, top, this._contentLeft, this._divWidth, this._lineHeight); } private _renderAtTopLeft(): void { - this._doRender(0, 0, this._contentWidth, 1); + this._doRender(0, 0, 0, this._contentWidth, 1); } - private _doRender(top: number, left: number, width: number, height: number): void { + private _doRender(scrollTop: number, top: number, left: number, width: number, height: number): void { // For correct alignment of the screen reader content, we need to apply the correct font applyFontInfo(this._domNode, this._fontInfo); @@ -142,16 +151,7 @@ export class ScreenReaderSupport { this._domNode.setLeft(left); this._domNode.setWidth(width); this._domNode.setHeight(height); - } - - private _setScrollTop(): void { - if (!this._screenReaderContentState) { - return; - } - // Setting position within the screen reader content by modifying scroll position - const textContentBeforeSelection = this._screenReaderContentState.value.substring(0, this._screenReaderContentState.selectionStart); - const numberOfLinesOfContentBeforeSelection = newlinecount(textContentBeforeSelection); - this._domNode.domNode.scrollTop = numberOfLinesOfContentBeforeSelection * this._lineHeight; + this._domNode.domNode.scrollTop = scrollTop; } public setAriaOptions(options: IEditorAriaOptions): void { diff --git a/src/vs/editor/browser/services/hoverService/hover.css b/src/vs/editor/browser/services/hoverService/hover.css index 21f7221bf68..8d483a54163 100644 --- a/src/vs/editor/browser/services/hoverService/hover.css +++ b/src/vs/editor/browser/services/hoverService/hover.css @@ -33,12 +33,8 @@ .monaco-workbench .workbench-hover-container.locked .workbench-hover { outline: 1px solid var(--vscode-editorHoverWidget-border); } -.monaco-workbench .workbench-hover-container.locked .workbench-hover:focus, -.monaco-workbench .workbench-hover-lock:focus { - outline: 1px solid var(--vscode-focusBorder); -} -.monaco-workbench .workbench-hover-container.locked .workbench-hover-lock:hover { - background: var(--vscode-toolbar-hoverBackground); +.monaco-workbench .workbench-hover-container:focus-within.locked .workbench-hover { + outline-color: var(--vscode-focusBorder); } .monaco-workbench .workbench-hover-pointer { @@ -57,12 +53,16 @@ border-right: 1px solid var(--vscode-editorHoverWidget-border); border-bottom: 1px solid var(--vscode-editorHoverWidget-border); } -.monaco-workbench .locked .workbench-hover-pointer:after { +.monaco-workbench .workbench-hover-container:not(:focus-within).locked .workbench-hover-pointer:after { width: 4px; height: 4px; border-right-width: 2px; border-bottom-width: 2px; } +.monaco-workbench .workbench-hover-container:focus-within .workbench-hover-pointer:after { + border-right: 1px solid var(--vscode-focusBorder); + border-bottom: 1px solid var(--vscode-focusBorder); +} .monaco-workbench .workbench-hover-pointer.left { left: -3px; } .monaco-workbench .workbench-hover-pointer.right { right: 3px; } diff --git a/src/vs/editor/browser/services/hoverService/hoverService.ts b/src/vs/editor/browser/services/hoverService/hoverService.ts index 7d249eb7065..523378c4c79 100644 --- a/src/vs/editor/browser/services/hoverService/hoverService.ts +++ b/src/vs/editor/browser/services/hoverService/hoverService.ts @@ -29,6 +29,7 @@ import { isNumber } from '../../../../base/common/types.js'; import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; +import { IMarkdownString } from '../../../../base/common/htmlContent.js'; export class HoverService extends Disposable implements IHoverService { declare readonly _serviceBrand: undefined; @@ -68,7 +69,7 @@ export class HoverService extends Disposable implements IHoverService { })); } - showHover(options: IHoverOptions, focus?: boolean, skipLastFocusedUpdate?: boolean, dontShow?: boolean): IHoverWidget | undefined { + showInstantHover(options: IHoverOptions, focus?: boolean, skipLastFocusedUpdate?: boolean, dontShow?: boolean): IHoverWidget | undefined { const hover = this._createHover(options, skipLastFocusedUpdate); if (!hover) { return undefined; @@ -81,6 +82,11 @@ export class HoverService extends Disposable implements IHoverService { options: IHoverOptions, lifecycleOptions: Pick, ): IHoverWidget | undefined { + // Set `id` to default if it's undefined + if (options.id === undefined) { + options.id = getHoverIdFromContent(options.content); + } + if (!this._currentDelayedHover || this._currentDelayedHoverWasShown) { // Current hover is locked, reject if (this._currentHover?.isLocked) { @@ -94,7 +100,7 @@ export class HoverService extends Disposable implements IHoverService { // Check group identity, if it's the same skip the delay and show the hover immediately if (this._currentHover && !this._currentHover.isDisposed && this._currentDelayedHoverGroupId !== undefined && this._currentDelayedHoverGroupId === lifecycleOptions?.groupId) { - return this.showHover({ + return this.showInstantHover({ ...options, appearance: { ...options.appearance, @@ -102,6 +108,9 @@ export class HoverService extends Disposable implements IHoverService { } }); } + } else if (this._currentDelayedHover && getHoverOptionsIdentity(this._currentHoverOptions) === getHoverOptionsIdentity(options)) { + // If the hover is the same but timeout is not finished yet, return the current hover + return this._currentDelayedHover; } const hover = this._createHover(options, undefined); @@ -118,7 +127,6 @@ export class HoverService extends Disposable implements IHoverService { timeout(this._configurationService.getValue('workbench.hover.delay')).then(() => { if (hover && !hover.isDisposed) { - this._currentDelayedHoverWasShown = true; this._currentDelayedHoverWasShown = true; this._showHover(hover, options); } @@ -169,12 +177,12 @@ export class HoverService extends Disposable implements IHoverService { store.add(addDisposableListener(target, EventType.KEY_DOWN, e => { const evt = new StandardKeyboardEvent(e); if (evt.equals(KeyCode.Space) || evt.equals(KeyCode.Enter)) { - this.showHover(resolveHoverOptions(), true); + this.showInstantHover(resolveHoverOptions(), true); } })); } - this._delayedHovers.set(target, { show: (focus: boolean) => { this.showHover(resolveHoverOptions(), focus); } }); + this._delayedHovers.set(target, { show: (focus: boolean) => { this.showInstantHover(resolveHoverOptions(), focus); } }); store.add(toDisposable(() => this._delayedHovers.delete(target))); return store; @@ -186,6 +194,12 @@ export class HoverService extends Disposable implements IHoverService { if (this._currentHover?.isLocked) { return undefined; } + + // Set `id` to default if it's undefined + if (options.id === undefined) { + options.id = getHoverIdFromContent(options.content); + } + if (getHoverOptionsIdentity(this._currentHoverOptions) === getHoverOptionsIdentity(options)) { return undefined; } @@ -204,15 +218,6 @@ export class HoverService extends Disposable implements IHoverService { } } - // Set `id` to default if it's undefined - if (options.id === undefined) { - options.id = isHTMLElement(options.content) - ? undefined - : typeof options.content === 'string' - ? options.content.toString() - : options.content.value; - } - const hoverDisposables = new DisposableStore(); const hover = this._instantiationService.createInstance(HoverWidget, options); if (options.persistence?.sticky) { @@ -291,8 +296,8 @@ export class HoverService extends Disposable implements IHoverService { ); } - hideHover(): void { - if (this._currentHover?.isLocked || !this._currentHoverOptions) { + hideHover(force?: boolean): void { + if ((!force && this._currentHover?.isLocked) || !this._currentHoverOptions) { return; } this.doHideHover(); @@ -315,7 +320,7 @@ export class HoverService extends Disposable implements IHoverService { if (!this._lastHoverOptions) { return; } - this.showHover(this._lastHoverOptions, true, true); + this.showInstantHover(this._lastHoverOptions, true, true); } private _showAndFocusHoverForActiveElement(): void { @@ -505,6 +510,16 @@ function getHoverOptionsIdentity(options: IHoverOptions | undefined): IHoverOpti return options?.id ?? options; } +function getHoverIdFromContent(content: string | HTMLElement | IMarkdownString): string | undefined { + if (isHTMLElement(content)) { + return undefined; + } + if (typeof content === 'string') { + return content.toString(); + } + return content.value; +} + class HoverContextViewDelegate implements IDelegate { // Render over all other context views diff --git a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts index c5b656b99d3..19a333e0035 100644 --- a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isHTMLElement } from '../../../../base/browser/dom.js'; -import type { IHoverWidget, IManagedHoverContent, IManagedHoverOptions } from '../../../../base/browser/ui/hover/hover.js'; +import { isManagedHoverTooltipMarkdownString, type IHoverWidget, type IManagedHoverContent, type IManagedHoverOptions } from '../../../../base/browser/ui/hover/hover.js'; import type { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; @@ -20,8 +20,7 @@ export class ManagedHoverWidget implements IDisposable { private _hoverWidget: IHoverWidget | undefined; private _cancellationTokenSource: CancellationTokenSource | undefined; - constructor(private hoverDelegate: IHoverDelegate, private target: IHoverDelegateTarget | HTMLElement, private fadeInAnimation: boolean) { - } + constructor(private hoverDelegate: IHoverDelegate, private target: IHoverDelegateTarget | HTMLElement, private fadeInAnimation: boolean) { } async update(content: IManagedHoverContent, focus?: boolean, options?: IManagedHoverOptions): Promise { if (this._cancellationTokenSource) { @@ -33,25 +32,37 @@ export class ManagedHoverWidget implements IDisposable { return; } - let resolvedContent; - if (content === undefined || isString(content) || isHTMLElement(content)) { + let resolvedContent: string | HTMLElement | IMarkdownString | undefined; + if (isString(content) || isHTMLElement(content) || content === undefined) { resolvedContent = content; - } else if (!isFunction(content.markdown)) { - resolvedContent = content.markdown ?? content.markdownNotSupportedFallback; } else { // compute the content, potentially long-running - // show 'Loading' if no hover is up yet - if (!this._hoverWidget) { - this.show(localize('iconLabel.loading', "Loading..."), focus, options); + this._cancellationTokenSource = new CancellationTokenSource(); + const token = this._cancellationTokenSource.token; + + let managedContent; + if (isManagedHoverTooltipMarkdownString(content)) { + if (isFunction(content.markdown)) { + managedContent = content.markdown(token).then(resolvedContent => resolvedContent ?? content.markdownNotSupportedFallback); + } else { + managedContent = content.markdown ?? content.markdownNotSupportedFallback; + } + } else { + managedContent = content.element(token); } // compute the content - this._cancellationTokenSource = new CancellationTokenSource(); - const token = this._cancellationTokenSource.token; - resolvedContent = await content.markdown(token); - if (resolvedContent === undefined) { - resolvedContent = content.markdownNotSupportedFallback; + if (managedContent instanceof Promise) { + + // show 'Loading' if no hover is up yet + if (!this._hoverWidget) { + this.show(localize('iconLabel.loading', "Loading..."), focus, options); + } + + resolvedContent = await managedContent; + } else { + resolvedContent = managedContent; } if (this.isDisposed || token.isCancellationRequested) { diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index e2a992cd339..53acaf14f7f 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -9,7 +9,7 @@ import { IMouseWheelEvent } from '../../base/browser/mouseEvent.js'; import { inputLatency } from '../../base/browser/performance.js'; import { CodeWindow } from '../../base/browser/window.js'; import { BugIndicatingError, onUnexpectedError } from '../../base/common/errors.js'; -import { IDisposable } from '../../base/common/lifecycle.js'; +import { Disposable, IDisposable } from '../../base/common/lifecycle.js'; import { IPointerHandlerHelper } from './controller/mouseHandler.js'; import { PointerHandlerLastRenderData } from './controller/mouseTarget.js'; import { PointerHandler } from './controller/pointerHandler.js'; @@ -63,6 +63,7 @@ import { NativeEditContext } from './controller/editContext/native/nativeEditCon import { RulersGpu } from './viewParts/rulersGpu/rulersGpu.js'; import { GpuMarkOverlay } from './viewParts/gpuMark/gpuMark.js'; import { AccessibilitySupport } from '../../platform/accessibility/common/accessibility.js'; +import { Event, Emitter } from '../../base/common/event.js'; export interface IContentWidgetData { @@ -82,6 +83,8 @@ export interface IGlyphMarginWidgetData { export class View extends ViewEventHandler { + private _widgetFocusTracker: CodeEditorWidgetFocusTracker; + private readonly _scrollbar: EditorScrollbar; private readonly _context: ViewContext; private readonly _viewGpuContext?: ViewGpuContext; @@ -116,6 +119,7 @@ export class View extends ViewEventHandler { private _ownerID: string; constructor( + editorContainer: HTMLElement, ownerID: string, commandDelegate: ICommandDelegate, configuration: IEditorConfiguration, @@ -127,6 +131,14 @@ export class View extends ViewEventHandler { ) { super(); this._ownerID = ownerID; + + this._widgetFocusTracker = this._register( + new CodeEditorWidgetFocusTracker(editorContainer, overflowWidgetsDomNode) + ); + this._register(this._widgetFocusTracker.onChange(() => { + this._context.viewModel.setHasWidgetFocus(this._widgetFocusTracker.hasFocus()); + })); + this._selections = [new Selection(1, 1, 1, 1)]; this._renderAnimationFrame = null; @@ -684,8 +696,13 @@ export class View extends ViewEventHandler { return this._editContext.isFocused(); } + public isWidgetFocused(): boolean { + return this._widgetFocusTracker.hasFocus(); + } + public refreshFocusState() { this._editContext.refreshFocusState(); + this._widgetFocusTracker.refreshState(); } public setAriaOptions(options: IEditorAriaOptions): void { @@ -851,3 +868,63 @@ class EditorRenderingCoordinator { } } +class CodeEditorWidgetFocusTracker extends Disposable { + + private _hasDomElementFocus: boolean; + private readonly _domFocusTracker: dom.IFocusTracker; + private readonly _overflowWidgetsDomNode: dom.IFocusTracker | undefined; + + private readonly _onChange: Emitter = this._register(new Emitter()); + public readonly onChange: Event = this._onChange.event; + + private _overflowWidgetsDomNodeHasFocus: boolean; + + private _hadFocus: boolean | undefined = undefined; + + constructor(domElement: HTMLElement, overflowWidgetsDomNode: HTMLElement | undefined) { + super(); + + this._hasDomElementFocus = false; + this._domFocusTracker = this._register(dom.trackFocus(domElement)); + + this._overflowWidgetsDomNodeHasFocus = false; + + this._register(this._domFocusTracker.onDidFocus(() => { + this._hasDomElementFocus = true; + this._update(); + })); + this._register(this._domFocusTracker.onDidBlur(() => { + this._hasDomElementFocus = false; + this._update(); + })); + + if (overflowWidgetsDomNode) { + this._overflowWidgetsDomNode = this._register(dom.trackFocus(overflowWidgetsDomNode)); + this._register(this._overflowWidgetsDomNode.onDidFocus(() => { + this._overflowWidgetsDomNodeHasFocus = true; + this._update(); + })); + this._register(this._overflowWidgetsDomNode.onDidBlur(() => { + this._overflowWidgetsDomNodeHasFocus = false; + this._update(); + })); + } + } + + private _update() { + const focused = this._hasDomElementFocus || this._overflowWidgetsDomNodeHasFocus; + if (this._hadFocus !== focused) { + this._hadFocus = focused; + this._onChange.fire(undefined); + } + } + + public hasFocus(): boolean { + return this._hadFocus ?? false; + } + + public refreshState(): void { + this._domFocusTracker.refreshState(); + this._overflowWidgetsDomNode?.refreshState?.(); + } +} diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 7e325a7f888..042152e7dde 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -231,8 +231,6 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _commandService: ICommandService; private readonly _themeService: IThemeService; - private readonly _focusTracker: CodeEditorWidgetFocusTracker; - private _contentWidgets: { [key: string]: IContentWidgetData }; private _overlayWidgets: { [key: string]: IOverlayWidgetData }; private _glyphMarginWidgets: { [key: string]: IGlyphMarginWidgetData }; @@ -306,11 +304,6 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._modelData = null; - this._focusTracker = new CodeEditorWidgetFocusTracker(domElement, this._overflowWidgetsDomNode); - this._register(this._focusTracker.onChange(() => { - this._editorWidgetFocus.setValue(this._focusTracker.hasFocus()); - })); - this._contentWidgets = {}; this._overlayWidgets = {}; this._glyphMarginWidgets = {}; @@ -406,7 +399,6 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE public override dispose(): void { this._codeEditorService.removeCodeEditor(this); - this._focusTracker.dispose(); this._actions.clear(); this._contentWidgets = {}; this._overlayWidgets = {}; @@ -1033,7 +1025,6 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE public onHide(): void { this._modelData?.view.refreshFocusState(); - this._focusTracker.refreshState(); } public getContribution(id: string): T | null { @@ -1451,7 +1442,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } public hasWidgetFocus(): boolean { - return this._focusTracker && this._focusTracker.hasFocus(); + if (!this._modelData || !this._modelData.hasRealView) { + return false; + } + return this._modelData.view.isWidgetFocused(); } public addContentWidget(widget: editorBrowser.IContentWidget): void { @@ -1690,6 +1684,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE case OutgoingViewModelEventKind.FocusChanged: this._editorTextFocus.setValue(e.hasFocus); break; + case OutgoingViewModelEventKind.WidgetFocusChanged: + this._editorWidgetFocus.setValue(e.hasFocus); + break; case OutgoingViewModelEventKind.ScrollChanged: this._onDidScrollChange.fire(e); break; @@ -1873,6 +1870,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE viewUserInputEvents.onMouseWheel = (e) => this._onMouseWheel.fire(e); const view = new View( + this._domElement, this.getId(), commandDelegate, this._configuration, @@ -2029,11 +2027,11 @@ const enum BooleanEventValue { } export class BooleanEventEmitter extends Disposable { - private readonly _onDidChangeToTrue: Emitter = this._register(new Emitter(this._emitterOptions)); - public readonly onDidChangeToTrue: Event = this._onDidChangeToTrue.event; + private readonly _onDidChangeToTrue: Emitter; + public readonly onDidChangeToTrue: Event; - private readonly _onDidChangeToFalse: Emitter = this._register(new Emitter(this._emitterOptions)); - public readonly onDidChangeToFalse: Event = this._onDidChangeToFalse.event; + private readonly _onDidChangeToFalse: Emitter; + public readonly onDidChangeToFalse: Event; private _value: BooleanEventValue; @@ -2041,6 +2039,10 @@ export class BooleanEventEmitter extends Disposable { private readonly _emitterOptions: EmitterOptions ) { super(); + this._onDidChangeToTrue = this._register(new Emitter(this._emitterOptions)); + this.onDidChangeToTrue = this._onDidChangeToTrue.event; + this._onDidChangeToFalse = this._register(new Emitter(this._emitterOptions)); + this.onDidChangeToFalse = this._onDidChangeToFalse.event; this._value = BooleanEventValue.NotSet; } @@ -2301,66 +2303,6 @@ export class EditorModeContext extends Disposable { } } -class CodeEditorWidgetFocusTracker extends Disposable { - - private _hasDomElementFocus: boolean; - private readonly _domFocusTracker: dom.IFocusTracker; - private readonly _overflowWidgetsDomNode: dom.IFocusTracker | undefined; - - private readonly _onChange: Emitter = this._register(new Emitter()); - public readonly onChange: Event = this._onChange.event; - - private _overflowWidgetsDomNodeHasFocus: boolean; - - private _hadFocus: boolean | undefined = undefined; - - constructor(domElement: HTMLElement, overflowWidgetsDomNode: HTMLElement | undefined) { - super(); - - this._hasDomElementFocus = false; - this._domFocusTracker = this._register(dom.trackFocus(domElement)); - - this._overflowWidgetsDomNodeHasFocus = false; - - this._register(this._domFocusTracker.onDidFocus(() => { - this._hasDomElementFocus = true; - this._update(); - })); - this._register(this._domFocusTracker.onDidBlur(() => { - this._hasDomElementFocus = false; - this._update(); - })); - - if (overflowWidgetsDomNode) { - this._overflowWidgetsDomNode = this._register(dom.trackFocus(overflowWidgetsDomNode)); - this._register(this._overflowWidgetsDomNode.onDidFocus(() => { - this._overflowWidgetsDomNodeHasFocus = true; - this._update(); - })); - this._register(this._overflowWidgetsDomNode.onDidBlur(() => { - this._overflowWidgetsDomNodeHasFocus = false; - this._update(); - })); - } - } - - private _update() { - const focused = this._hasDomElementFocus || this._overflowWidgetsDomNodeHasFocus; - if (this._hadFocus !== focused) { - this._hadFocus = focused; - this._onChange.fire(undefined); - } - } - - public hasFocus(): boolean { - return this._hadFocus ?? false; - } - - public refreshState(): void { - this._domFocusTracker.refreshState(); - this._overflowWidgetsDomNode?.refreshState?.(); - } -} class EditorDecorationsCollection implements editorCommon.IEditorDecorationsCollection { diff --git a/src/vs/editor/browser/widget/diffEditor/style.css b/src/vs/editor/browser/widget/diffEditor/style.css index 8c027c33e1c..8a461de93ef 100644 --- a/src/vs/editor/browser/widget/diffEditor/style.css +++ b/src/vs/editor/browser/widget/diffEditor/style.css @@ -382,7 +382,7 @@ .actions-container { width: fit-content; border-radius: 4px; - background: var(--vscode-editorGutter-commentRangeForeground); + background: var(--vscode-editorGutter-itemBackground); .action-item { &:hover { @@ -390,6 +390,7 @@ } .action-label { + color: var(--vscode-editorGutter-itemGlyphForeground); padding: 1px 2px; } } diff --git a/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts b/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts index 3703fa1df29..9da1394cf82 100644 --- a/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts +++ b/src/vs/editor/common/codecs/markdownCodec/markdownDecoder.ts @@ -3,232 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MarkdownLink } from './tokens/markdownLink.js'; -import { NewLine } from '../linesCodec/tokens/newLine.js'; -import { assert } from '../../../../base/common/assert.js'; -import { FormFeed } from '../simpleCodec/tokens/formFeed.js'; +import { MarkdownToken } from './tokens/markdownToken.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; -import { VerticalTab } from '../simpleCodec/tokens/verticalTab.js'; +import { LeftBracket } from '../simpleCodec/tokens/brackets.js'; import { ReadableStream } from '../../../../base/common/stream.js'; -import { CarriageReturn } from '../linesCodec/tokens/carriageReturn.js'; +import { LeftAngleBracket } from '../simpleCodec/tokens/angleBrackets.js'; import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; -import { LeftBracket, RightBracket } from '../simpleCodec/tokens/brackets.js'; import { SimpleDecoder, TSimpleToken } from '../simpleCodec/simpleDecoder.js'; -import { ParserBase, TAcceptTokenResult } from '../simpleCodec/parserBase.js'; -import { LeftParenthesis, RightParenthesis } from '../simpleCodec/tokens/parentheses.js'; +import { MarkdownCommentStart, PartialMarkdownCommentStart } from './parsers/markdownComment.js'; +import { MarkdownLinkCaption, PartialMarkdownLink, PartialMarkdownLinkCaption } from './parsers/markdownLink.js'; +import { ExclamationMark } from '../simpleCodec/tokens/exclamationMark.js'; +import { PartialMarkdownImage } from './parsers/markdownImage.js'; /** * Tokens handled by this decoder. */ -export type TMarkdownToken = MarkdownLink | TSimpleToken; +export type TMarkdownToken = MarkdownToken | TSimpleToken; /** - * List of characters that stop a markdown link sequence. - */ -const MARKDOWN_LINK_STOP_CHARACTERS: readonly string[] = [CarriageReturn, NewLine, VerticalTab, FormFeed] - .map((token) => { return token.symbol; }); - -/** - * The parser responsible for parsing a `markdown link caption` part of a markdown - * link (e.g., the `[caption text]` part of the `[caption text](./some/path)` link). - * - * The parsing process starts with single `[` token and collects all tokens until - * the first `]` token is encountered. In this successful case, the parser transitions - * into the {@linkcode MarkdownLinkCaption} parser type which continues the general - * parsing process of the markdown link. - * - * Otherwise, if one of the stop characters defined in the {@linkcode MARKDOWN_LINK_STOP_CHARACTERS} - * is encountered before the `]` token, the parsing process is aborted which is communicated to - * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible - * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no - * longer represent a coherent token entity of a larger size. - */ -class PartialMarkdownLinkCaption extends ParserBase { - constructor(token: LeftBracket) { - super([token]); - } - - public accept(token: TSimpleToken): TAcceptTokenResult { - // any of stop characters is are breaking a markdown link caption sequence - if (MARKDOWN_LINK_STOP_CHARACTERS.includes(token.text)) { - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - // the `]` character ends the caption of a markdown link - if (token instanceof RightBracket) { - return { - result: 'success', - nextParser: new MarkdownLinkCaption([...this.tokens, token]), - wasTokenConsumed: true, - }; - } - - // otherwise, include the token in the sequence - // and keep the current parser object instance - this.currentTokens.push(token); - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } -} - -/** - * The parser responsible for transitioning from a {@linkcode PartialMarkdownLinkCaption} - * parser to the {@link PartialMarkdownLink} one, therefore serves a parser glue between - * the `[caption]` and the `(./some/path)` parts of the `[caption](./some/path)` link. - * - * The only successful case of this parser is the `(` token that initiated the process - * of parsing the `reference` part of a markdown link and in this case the parser - * transitions into the `PartialMarkdownLink` parser type. - * - * Any other character is considered a failure result. In this case, the caller is assumed - * to be responsible for re-emitting the {@link tokens} accumulated so far as standalone - * entities since they are no longer represent a coherent token entity of a larger size. - */ -class MarkdownLinkCaption extends ParserBase { - public accept(token: TSimpleToken): TAcceptTokenResult { - // the `(` character starts the link part of a markdown link - // that is the only character that can follow the caption - if (token instanceof LeftParenthesis) { - return { - result: 'success', - wasTokenConsumed: true, - nextParser: new PartialMarkdownLink([...this.tokens], token), - }; - } - - return { - result: 'failure', - wasTokenConsumed: false, - }; - } -} - -/** - * The parser responsible for parsing a `link reference` part of a markdown link - * (e.g., the `(./some/path)` part of the `[caption text](./some/path)` link). - * - * The parsing process starts with tokens that represent the `[caption]` part of a markdown - * link, followed by the `(` token. The parser collects all subsequent tokens until final closing - * parenthesis (`)`) is encountered (*\*see [1] below*). In this successful case, the parser object - * transitions into the {@linkcode MarkdownLink} token type which signifies the end of the entire - * parsing process of the link text. - * - * Otherwise, if one of the stop characters defined in the {@linkcode MARKDOWN_LINK_STOP_CHARACTERS} - * is encountered before the final `)` token, the parsing process is aborted which is communicated to - * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible - * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no - * longer represent a coherent token entity of a larger size. - * - * `[1]` The `reference` part of the markdown link can contain any number of nested parenthesis, e.g., - * `[caption](/some/p(th/file.md)` is a valid markdown link and a valid folder name, hence number - * of open parenthesis must match the number of closing ones and the path sequence is considered - * to be complete as soon as this requirement is met. Therefore the `final` word is used in - * the description comments above to highlight this important detail. - */ -class PartialMarkdownLink extends ParserBase { - /** - * Number of open parenthesis in the sequence. - * See comment in the {@linkcode accept} method for more details. - */ - private openParensCount: number = 1; - - constructor( - protected readonly captionTokens: TSimpleToken[], - token: LeftParenthesis, - ) { - super([token]); - } - - public override get tokens(): readonly TSimpleToken[] { - return [...this.captionTokens, ...this.currentTokens]; - } - - public accept(token: TSimpleToken): TAcceptTokenResult { - // markdown links allow for nested parenthesis inside the link reference part, but - // the number of open parenthesis must match the number of closing parenthesis, e.g.: - // - `[caption](/some/p()th/file.md)` is a valid markdown link - // - `[caption](/some/p(th/file.md)` is an invalid markdown link - // hence we use the `openParensCount` variable to keep track of the number of open - // parenthesis encountered so far; then upon encountering a closing parenthesis we - // decrement the `openParensCount` and if it reaches 0 - we consider the link reference - // to be complete - - if (token instanceof LeftParenthesis) { - this.openParensCount += 1; - } - - if (token instanceof RightParenthesis) { - this.openParensCount -= 1; - - // sanity check! this must alway hold true because we return a complete markdown - // link as soon as we encounter matching number of closing parenthesis, hence - // we must never have `openParensCount` that is less than 0 - assert( - this.openParensCount >= 0, - `Unexpected right parenthesis token encountered: '${token}'.`, - ); - - // the markdown link is complete as soon as we get the same number of closing parenthesis - if (this.openParensCount === 0) { - const { startLineNumber, startColumn } = this.captionTokens[0].range; - - // create link caption string - const caption = this.captionTokens - .map((token) => { return token.text; }) - .join(''); - - // create link reference string - this.currentTokens.push(token); - const reference = this.currentTokens - .map((token) => { return token.text; }).join(''); - - // return complete markdown link object - return { - result: 'success', - wasTokenConsumed: true, - nextParser: new MarkdownLink( - startLineNumber, - startColumn, - caption, - reference, - ), - }; - } - } - - // any of stop characters is are breaking a markdown link reference sequence - if (MARKDOWN_LINK_STOP_CHARACTERS.includes(token.text)) { - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - // the rest of the tokens can be included in the sequence - this.currentTokens.push(token); - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } -} - -/** - * Decoder capable of parsing markdown entities (e.g., links) from a sequence of simplier tokens. + * Decoder capable of parsing markdown entities (e.g., links) from a sequence of simple tokens. */ export class MarkdownDecoder extends BaseDecoder { /** - * Current parser object that is responsible for parsing a sequence of tokens - * into some markdown entity. + * Current parser object that is responsible for parsing a sequence of tokens into + * some markdown entity. Set to `undefined` when no parsing is in progress at the moment. */ - private current?: PartialMarkdownLinkCaption | MarkdownLinkCaption | PartialMarkdownLink; + private current?: + PartialMarkdownLinkCaption | MarkdownLinkCaption | PartialMarkdownLink | + PartialMarkdownCommentStart | MarkdownCommentStart | + PartialMarkdownImage; constructor( stream: ReadableStream, @@ -237,7 +40,7 @@ export class MarkdownDecoder extends BaseDecoder { } protected override onStreamData(token: TSimpleToken): void { - // markdown links start with `[` character, so here we can + // `markdown links` start with `[` character, so here we can // initiate the process of parsing a markdown link if (token instanceof LeftBracket && !this.current) { this.current = new PartialMarkdownLinkCaption(token); @@ -245,9 +48,24 @@ export class MarkdownDecoder extends BaseDecoder { return; } - // if current parser was not initiated before, - we are not inside a - // sequence of tokens we care about, therefore re-emit the token - // immediately and continue to the next one + // `markdown comments` start with `<` character, so here we can + // initiate the process of parsing a markdown comment + if (token instanceof LeftAngleBracket && !this.current) { + this.current = new PartialMarkdownCommentStart(token); + + return; + } + + // `markdown image links` start with `!` character, so here we can + // initiate the process of parsing a markdown image + if (token instanceof ExclamationMark && !this.current) { + this.current = new PartialMarkdownImage(token); + + return; + } + + // if current parser was not initiated before, - we are not inside a sequence + // of tokens we care about, therefore re-emit the token immediately and continue if (!this.current) { this._onData.fire(token); return; @@ -257,14 +75,16 @@ export class MarkdownDecoder extends BaseDecoder { // so it can progress with parsing the tokens sequence const parseResult = this.current.accept(token); if (parseResult.result === 'success') { - // if got a parsed out `MarkdownLink` back, emit it - // then reset the current parser object - if (parseResult.nextParser instanceof MarkdownLink) { - this._onData.fire(parseResult.nextParser); + const { nextParser } = parseResult; + + // if got a fully parsed out token back, emit it and reset + // the current parser object so a new parsing process can start + if (nextParser instanceof MarkdownToken) { + this._onData.fire(nextParser); delete this.current; } else { // otherwise, update the current parser object - this.current = parseResult.nextParser; + this.current = nextParser; } } else { // if failed to parse a sequence of a tokens as a single markdown @@ -286,8 +106,18 @@ export class MarkdownDecoder extends BaseDecoder { protected override onStreamEnd(): void { // if the stream has ended and there is a current incomplete parser - // object present, then re-emit its tokens as standalone entities + // object present, handle the remaining parser object if (this.current) { + // if a `markdown comment` does not have an end marker `-->` + // it is still a comment that extends to the end of the file + // so re-emit the current parser as a comment token + if (this.current instanceof MarkdownCommentStart) { + this._onData.fire(this.current.asMarkdownComment()); + delete this.current; + return this.onStreamEnd(); + } + + // in all other cases, re-emit existing parser tokens const { tokens } = this.current; delete this.current; diff --git a/src/vs/editor/common/codecs/markdownCodec/parsers/markdownComment.ts b/src/vs/editor/common/codecs/markdownCodec/parsers/markdownComment.ts new file mode 100644 index 00000000000..df5f3f028ab --- /dev/null +++ b/src/vs/editor/common/codecs/markdownCodec/parsers/markdownComment.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range } from '../../../core/range.js'; +import { Dash } from '../../simpleCodec/tokens/dash.js'; +import { pick } from '../../../../../base/common/arrays.js'; +import { assert } from '../../../../../base/common/assert.js'; +import { MarkdownComment } from '../tokens/markdownComment.js'; +import { TSimpleToken } from '../../simpleCodec/simpleDecoder.js'; +import { ExclamationMark } from '../../simpleCodec/tokens/exclamationMark.js'; +import { LeftAngleBracket, RightAngleBracket } from '../../simpleCodec/tokens/angleBrackets.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; + +/** + * The parser responsible for parsing the ``. If it does, + * then the parser transitions to the {@link MarkdownComment} token. + */ +export class MarkdownCommentStart extends ParserBase { + constructor(tokens: [LeftAngleBracket, ExclamationMark, Dash, Dash]) { + super(tokens); + } + + @assertNotConsumed + public accept(token: TSimpleToken): TAcceptTokenResult { + // if received `>` while current token sequence ends with `--`, + // then this is the end of the comment sequence + if (token instanceof RightAngleBracket && this.endsWithDashes) { + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this.asMarkdownComment(), + wasTokenConsumed: true, + }; + } + + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + /** + * Convert the current token sequence into a {@link MarkdownComment} token. + * + * Note! that this method marks the current parser object as "consumend" + * hence it should not be used after this method is called. + */ + public asMarkdownComment(): MarkdownComment { + this.isConsumed = true; + + const text = this.currentTokens + .map(pick('text')) + .join(''); + + return new MarkdownComment( + this.range, + text, + ); + } + + /** + * Get range of current token sequence. + */ + private get range(): Range { + const firstToken = this.currentTokens[0]; + const lastToken = this.currentTokens[this.currentTokens.length - 1]; + + const range = new Range( + firstToken.range.startLineNumber, + firstToken.range.startColumn, + lastToken.range.endLineNumber, + lastToken.range.endColumn, + ); + + return range; + } + + /** + * Whether the current token sequence ends with two dashes. + */ + private get endsWithDashes(): boolean { + const lastToken = this.currentTokens[this.currentTokens.length - 1]; + if (!(lastToken instanceof Dash)) { + return false; + } + + const secondLastToken = this.currentTokens[this.currentTokens.length - 2]; + if (!(secondLastToken instanceof Dash)) { + return false; + } + + return true; + } +} diff --git a/src/vs/editor/common/codecs/markdownCodec/parsers/markdownImage.ts b/src/vs/editor/common/codecs/markdownCodec/parsers/markdownImage.ts new file mode 100644 index 00000000000..a3264f7cbdd --- /dev/null +++ b/src/vs/editor/common/codecs/markdownCodec/parsers/markdownImage.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownLink } from '../tokens/markdownLink.js'; +import { MarkdownImage } from '../tokens/markdownImage.js'; +import { TSimpleToken } from '../../simpleCodec/simpleDecoder.js'; +import { LeftBracket } from '../../simpleCodec/tokens/brackets.js'; +import { ExclamationMark } from '../../simpleCodec/tokens/exclamationMark.js'; +import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; +import { MarkdownLinkCaption, PartialMarkdownLink, PartialMarkdownLinkCaption } from './markdownLink.js'; + +/** + * The parser responsible for parsing the `markdown image` sequence of characters. + * E.g., `![alt text](./path/to/image.jpeg)` syntax. + */ +export class PartialMarkdownImage extends ParserBase { + /** + * Current active parser instance, if in the mode of actively parsing the markdown link sequence. + */ + private markdownLinkParser: PartialMarkdownLinkCaption | MarkdownLinkCaption | PartialMarkdownLink | undefined; + + constructor(token: ExclamationMark) { + super([token]); + } + + /** + * Get all currently available tokens of the `markdown link` sequence. + */ + public override get tokens(): readonly TSimpleToken[] { + const linkTokens = this.markdownLinkParser?.tokens ?? []; + + return [ + ...this.currentTokens, + ...linkTokens, + ]; + } + + @assertNotConsumed + public accept(token: TSimpleToken): TAcceptTokenResult { + // on the first call we expect a character that begins `markdown link` sequence + // hence we initiate the markdown link parsing process, otherwise we fail + if (!this.markdownLinkParser) { + if (token instanceof LeftBracket) { + this.markdownLinkParser = new PartialMarkdownLinkCaption(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // handle subsequent tokens next + + const acceptResult = this.markdownLinkParser.accept(token); + const { result, wasTokenConsumed } = acceptResult; + + if (result === 'success') { + const { nextParser } = acceptResult; + + // if full markdown link was parsed out, the process completes + if (nextParser instanceof MarkdownLink) { + this.isConsumed = true; + + const firstToken = this.currentTokens[0]; + return { + result, + wasTokenConsumed, + nextParser: new MarkdownImage( + firstToken.range.startLineNumber, + firstToken.range.startColumn, + `${firstToken.text}${nextParser.caption}`, + nextParser.reference, + ), + }; + } + + // otherwise save new link parser reference and continue + this.markdownLinkParser = nextParser; + return { + result, + wasTokenConsumed, + nextParser: this, + }; + } + + // return the failure result + this.isConsumed = true; + return acceptResult; + } +} diff --git a/src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts b/src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts new file mode 100644 index 00000000000..e8163286fd2 --- /dev/null +++ b/src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts @@ -0,0 +1,213 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownLink } from '../tokens/markdownLink.js'; +import { NewLine } from '../../linesCodec/tokens/newLine.js'; +import { assert } from '../../../../../base/common/assert.js'; +import { FormFeed } from '../../simpleCodec/tokens/formFeed.js'; +import { TSimpleToken } from '../../simpleCodec/simpleDecoder.js'; +import { VerticalTab } from '../../simpleCodec/tokens/verticalTab.js'; +import { CarriageReturn } from '../../linesCodec/tokens/carriageReturn.js'; +import { LeftBracket, RightBracket } from '../../simpleCodec/tokens/brackets.js'; +import { ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; +import { LeftParenthesis, RightParenthesis } from '../../simpleCodec/tokens/parentheses.js'; + +/** + * List of characters that are not allowed in links so stop a markdown link sequence abruptly. + */ +const MARKDOWN_LINK_STOP_CHARACTERS: readonly string[] = [CarriageReturn, NewLine, VerticalTab, FormFeed] + .map((token) => { return token.symbol; }); + +/** + * The parser responsible for parsing a `markdown link caption` part of a markdown + * link (e.g., the `[caption text]` part of the `[caption text](./some/path)` link). + * + * The parsing process starts with single `[` token and collects all tokens until + * the first `]` token is encountered. In this successful case, the parser transitions + * into the {@linkcode MarkdownLinkCaption} parser type which continues the general + * parsing process of the markdown link. + * + * Otherwise, if one of the stop characters defined in the {@linkcode MARKDOWN_LINK_STOP_CHARACTERS} + * is encountered before the `]` token, the parsing process is aborted which is communicated to + * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible + * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no + * longer represent a coherent token entity of a larger size. + */ +export class PartialMarkdownLinkCaption extends ParserBase { + constructor(token: LeftBracket) { + super([token]); + } + + public accept(token: TSimpleToken): TAcceptTokenResult { + // any of stop characters is are breaking a markdown link caption sequence + if (MARKDOWN_LINK_STOP_CHARACTERS.includes(token.text)) { + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // the `]` character ends the caption of a markdown link + if (token instanceof RightBracket) { + return { + result: 'success', + nextParser: new MarkdownLinkCaption([...this.tokens, token]), + wasTokenConsumed: true, + }; + } + + // otherwise, include the token in the sequence + // and keep the current parser object instance + this.currentTokens.push(token); + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } +} + +/** + * The parser responsible for transitioning from a {@linkcode PartialMarkdownLinkCaption} + * parser to the {@link PartialMarkdownLink} one, therefore serves a parser glue between + * the `[caption]` and the `(./some/path)` parts of the `[caption](./some/path)` link. + * + * The only successful case of this parser is the `(` token that initiated the process + * of parsing the `reference` part of a markdown link and in this case the parser + * transitions into the `PartialMarkdownLink` parser type. + * + * Any other character is considered a failure result. In this case, the caller is assumed + * to be responsible for re-emitting the {@link tokens} accumulated so far as standalone + * entities since they are no longer represent a coherent token entity of a larger size. + */ +export class MarkdownLinkCaption extends ParserBase { + public accept(token: TSimpleToken): TAcceptTokenResult { + // the `(` character starts the link part of a markdown link + // that is the only character that can follow the caption + if (token instanceof LeftParenthesis) { + return { + result: 'success', + wasTokenConsumed: true, + nextParser: new PartialMarkdownLink([...this.tokens], token), + }; + } + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } +} + +/** + * The parser responsible for parsing a `link reference` part of a markdown link + * (e.g., the `(./some/path)` part of the `[caption text](./some/path)` link). + * + * The parsing process starts with tokens that represent the `[caption]` part of a markdown + * link, followed by the `(` token. The parser collects all subsequent tokens until final closing + * parenthesis (`)`) is encountered (*\*see [1] below*). In this successful case, the parser object + * transitions into the {@linkcode MarkdownLink} token type which signifies the end of the entire + * parsing process of the link text. + * + * Otherwise, if one of the stop characters defined in the {@linkcode MARKDOWN_LINK_STOP_CHARACTERS} + * is encountered before the final `)` token, the parsing process is aborted which is communicated to + * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible + * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no + * longer represent a coherent token entity of a larger size. + * + * `[1]` The `reference` part of the markdown link can contain any number of nested parenthesis, e.g., + * `[caption](/some/p(th/file.md)` is a valid markdown link and a valid folder name, hence number + * of open parenthesis must match the number of closing ones and the path sequence is considered + * to be complete as soon as this requirement is met. Therefore the `final` word is used in + * the description comments above to highlight this important detail. + */ +export class PartialMarkdownLink extends ParserBase { + /** + * Number of open parenthesis in the sequence. + * See comment in the {@linkcode accept} method for more details. + */ + private openParensCount: number = 1; + + constructor( + protected readonly captionTokens: TSimpleToken[], + token: LeftParenthesis, + ) { + super([token]); + } + + public override get tokens(): readonly TSimpleToken[] { + return [...this.captionTokens, ...this.currentTokens]; + } + + public accept(token: TSimpleToken): TAcceptTokenResult { + // markdown links allow for nested parenthesis inside the link reference part, but + // the number of open parenthesis must match the number of closing parenthesis, e.g.: + // - `[caption](/some/p()th/file.md)` is a valid markdown link + // - `[caption](/some/p(th/file.md)` is an invalid markdown link + // hence we use the `openParensCount` variable to keep track of the number of open + // parenthesis encountered so far; then upon encountering a closing parenthesis we + // decrement the `openParensCount` and if it reaches 0 - we consider the link reference + // to be complete + + if (token instanceof LeftParenthesis) { + this.openParensCount += 1; + } + + if (token instanceof RightParenthesis) { + this.openParensCount -= 1; + + // sanity check! this must alway hold true because we return a complete markdown + // link as soon as we encounter matching number of closing parenthesis, hence + // we must never have `openParensCount` that is less than 0 + assert( + this.openParensCount >= 0, + `Unexpected right parenthesis token encountered: '${token}'.`, + ); + + // the markdown link is complete as soon as we get the same number of closing parenthesis + if (this.openParensCount === 0) { + const { startLineNumber, startColumn } = this.captionTokens[0].range; + + // create link caption string + const caption = this.captionTokens + .map((token) => { return token.text; }) + .join(''); + + // create link reference string + this.currentTokens.push(token); + const reference = this.currentTokens + .map((token) => { return token.text; }).join(''); + + // return complete markdown link object + return { + result: 'success', + wasTokenConsumed: true, + nextParser: new MarkdownLink( + startLineNumber, + startColumn, + caption, + reference, + ), + }; + } + } + + // any of stop characters is are breaking a markdown link reference sequence + if (MARKDOWN_LINK_STOP_CHARACTERS.includes(token.text)) { + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // the rest of the tokens can be included in the sequence + this.currentTokens.push(token); + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } +} diff --git a/src/vs/editor/common/codecs/markdownCodec/tokens/markdownComment.ts b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownComment.ts new file mode 100644 index 00000000000..f7875d957a2 --- /dev/null +++ b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownComment.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { MarkdownToken } from './markdownToken.js'; +import { assert } from '../../../../../base/common/assert.js'; + +/** + * A token that represent a `markdown comment` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class MarkdownComment extends MarkdownToken { + constructor( + range: Range, + public readonly text: string, + ) { + assert( + text.startsWith('`. + */ + public get hasEndMarker(): boolean { + return this.text.endsWith('-->'); + } + + /** + * Check if this token is equal to another one. + */ + public override equals(other: T): boolean { + if (!super.sameRange(other.range)) { + return false; + } + + if (!(other instanceof MarkdownComment)) { + return false; + } + + return this.text === other.text; + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `md-comment("${this.text}")${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/markdownCodec/tokens/markdownImage.ts b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownImage.ts new file mode 100644 index 00000000000..75e42e31aaf --- /dev/null +++ b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownImage.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { MarkdownToken } from './markdownToken.js'; +import { IRange, Range } from '../../../core/range.js'; +import { assert } from '../../../../../base/common/assert.js'; + +/** + * A token that represent a `markdown image` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class MarkdownImage extends MarkdownToken { + /** + * Check if this `markdown image link` points to a valid URL address. + */ + public readonly isURL: boolean; + + constructor( + /** + * The starting line number of the image (1-based indexing). + */ + lineNumber: number, + /** + * The starting column number of the image (1-based indexing). + */ + columnNumber: number, + /** + * The caption of the image, including the `!` and `square brackets`. + */ + private readonly caption: string, + /** + * The reference of the image, including the parentheses. + */ + private readonly reference: string, + ) { + assert( + !isNaN(lineNumber), + `The line number must not be a NaN.`, + ); + + assert( + lineNumber > 0, + `The line number must be >= 1, got "${lineNumber}".`, + ); + + assert( + columnNumber > 0, + `The column number must be >= 1, got "${columnNumber}".`, + ); + + assert( + caption[0] === '!', + `The caption must start with '!' character, got "${caption}".`, + ); + + assert( + caption[1] === '[' && caption[caption.length - 1] === ']', + `The caption must be enclosed in square brackets, got "${caption}".`, + ); + + assert( + reference[0] === '(' && reference[reference.length - 1] === ')', + `The reference must be enclosed in parentheses, got "${reference}".`, + ); + + super( + new Range( + lineNumber, + columnNumber, + lineNumber, + columnNumber + caption.length + reference.length, + ), + ); + + // set up the `isURL` flag based on the current + try { + new URL(this.path); + this.isURL = true; + } catch { + this.isURL = false; + } + } + + public override get text(): string { + return `${this.caption}${this.reference}`; + } + + /** + * Returns the `reference` part of the link without enclosing parentheses. + */ + public get path(): string { + return this.reference.slice(1, this.reference.length - 1); + } + + /** + * Check if this token is equal to another one. + */ + public override equals(other: T): boolean { + if (!super.sameRange(other.range)) { + return false; + } + + if (!(other instanceof MarkdownImage)) { + return false; + } + + return this.text === other.text; + } + + /** + * Get the range of the `link part` of the token. + */ + public get linkRange(): IRange | undefined { + if (this.path.length === 0) { + return undefined; + } + + const { range } = this; + + // note! '+1' for openning `(` of the link + const startColumn = range.startColumn + this.caption.length + 1; + const endColumn = startColumn + this.path.length; + + return new Range( + range.startLineNumber, + startColumn, + range.endLineNumber, + endColumn, + ); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `md-image("${this.text}")${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts index b4c8947a213..a4b15718717 100644 --- a/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts +++ b/src/vs/editor/common/codecs/markdownCodec/tokens/markdownLink.ts @@ -28,13 +28,13 @@ export class MarkdownLink extends MarkdownToken { */ columnNumber: number, /** - * The caption of the link, including the square brackets. + * The caption of the original link, including the square brackets. */ - private readonly caption: string, + public readonly caption: string, /** - * The reference of the link, including the parentheses. + * The reference of the original link, including the parentheses. */ - private readonly reference: string, + public readonly reference: string, ) { assert( !isNaN(lineNumber), diff --git a/src/vs/editor/common/codecs/simpleCodec/parserBase.ts b/src/vs/editor/common/codecs/simpleCodec/parserBase.ts index 9e864177f9f..e088a18f264 100644 --- a/src/vs/editor/common/codecs/simpleCodec/parserBase.ts +++ b/src/vs/editor/common/codecs/simpleCodec/parserBase.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { BaseToken } from '../baseToken.js'; +import { assert } from '../../../../base/common/assert.js'; /** * Common interface for a result of accepting a next token @@ -47,12 +48,24 @@ export type TAcceptTokenResult = IAcceptTokenSuccess | IAcceptTokenFailure * tokens into a new single entity. */ export abstract class ParserBase { + /** + * Whether the parser object was "consumed" and should not be used anymore. + */ + protected isConsumed: boolean = false; + + /** + * Number of tokens at the initialization of the current parser. + */ + protected readonly startTokensCount: number; + constructor( /** * Set of tokens that were accumulated so far. */ protected readonly currentTokens: TToken[] = [], - ) { } + ) { + this.startTokensCount = this.currentTokens.length; + } /** * Get the tokens that were accumulated so far. @@ -70,4 +83,48 @@ export abstract class ParserBase { * @returns The parsing result. */ public abstract accept(token: TToken): TAcceptTokenResult; + + /** + * A helper method that validates that the current parser object was not yet consumed, + * hence can still be used to accept new tokens in the parsing process. + * + * @throws if the parser object is already consumed. + */ + protected assertNotConsumed(): void { + assert( + this.isConsumed === false, + `The parser object is already consumed and should not be used anymore.`, + ); + } +} + +/** + * Decorator that validates that the current parser object was not yet consumed, + * hence can still be used to accept new tokens in the parsing process. + * + * @throws the resulting decorated method throws if the parser object was already consumed. + */ +export function assertNotConsumed>( + _target: T, + propertyKey: 'accept', + descriptor: PropertyDescriptor, +) { + // store the original method reference + const originalMethod = descriptor.value; + + // validate that the current parser object was not yet consumed + // before invoking the original accept method + descriptor.value = function ( + this: T, + ...args: Parameters + ): ReturnType { + assert( + this.isConsumed === false, + `The parser object is already consumed and should not be used anymore.`, + ); + + return originalMethod.apply(this, args); + }; + + return descriptor; } diff --git a/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts b/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts index 88ad1298501..c32542f28da 100644 --- a/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts +++ b/src/vs/editor/common/codecs/simpleCodec/simpleDecoder.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Hash } from './tokens/hash.js'; +import { Dash } from './tokens/dash.js'; import { Colon } from './tokens/colon.js'; import { FormFeed } from './tokens/formFeed.js'; import { Tab } from '../simpleCodec/tokens/tab.js'; @@ -12,39 +13,44 @@ import { VerticalTab } from './tokens/verticalTab.js'; import { Space } from '../simpleCodec/tokens/space.js'; import { NewLine } from '../linesCodec/tokens/newLine.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; -import { LeftBracket, RightBracket } from './tokens/brackets.js'; +import { ExclamationMark } from './tokens/exclamationMark.js'; import { ReadableStream } from '../../../../base/common/stream.js'; import { CarriageReturn } from '../linesCodec/tokens/carriageReturn.js'; import { LinesDecoder, TLineToken } from '../linesCodec/linesDecoder.js'; +import { LeftBracket, RightBracket, TBracket } from './tokens/brackets.js'; import { BaseDecoder } from '../../../../base/common/codecs/baseDecoder.js'; -import { LeftParenthesis, RightParenthesis } from './tokens/parentheses.js'; +import { LeftParenthesis, RightParenthesis, TParenthesis } from './tokens/parentheses.js'; +import { LeftAngleBracket, RightAngleBracket, TAngleBracket } from './tokens/angleBrackets.js'; /** * A token type that this decoder can handle. */ -export type TSimpleToken = Word | Space | Tab | VerticalTab | NewLine | FormFeed | CarriageReturn | LeftBracket - | RightBracket | LeftParenthesis | RightParenthesis | Colon | Hash; +export type TSimpleToken = Word | Space | Tab | VerticalTab | NewLine | FormFeed + | CarriageReturn | TBracket | TAngleBracket | TParenthesis + | Colon | Hash | Dash | ExclamationMark; /** * List of well-known distinct tokens that this decoder emits (excluding * the word stop characters defined below). Everything else is considered * an arbitrary "text" sequence and is emitted as a single `Word` token. */ -const WELL_KNOWN_TOKENS = [ - Space, Tab, VerticalTab, FormFeed, LeftBracket, RightBracket, - LeftParenthesis, RightParenthesis, Colon, Hash, -]; +const WELL_KNOWN_TOKENS = Object.freeze([ + Space, Tab, VerticalTab, FormFeed, + LeftBracket, RightBracket, LeftAngleBracket, RightAngleBracket, + LeftParenthesis, RightParenthesis, Colon, Hash, Dash, ExclamationMark, +]); /** * Characters that stop a "word" sequence. * Note! the `\r` and `\n` are excluded from the list because this decoder based on `LinesDecoder` which * already handles the `carriagereturn`/`newline` cases and emits lines that don't contain them. */ -const WORD_STOP_CHARACTERS = [ +const WORD_STOP_CHARACTERS: readonly string[] = Object.freeze([ Space.symbol, Tab.symbol, VerticalTab.symbol, FormFeed.symbol, - LeftBracket.symbol, RightBracket.symbol, LeftParenthesis.symbol, - RightParenthesis.symbol, Colon.symbol, Hash.symbol, -]; + LeftBracket.symbol, RightBracket.symbol, LeftAngleBracket.symbol, RightAngleBracket.symbol, + LeftParenthesis.symbol, RightParenthesis.symbol, + Colon.symbol, Hash.symbol, Dash.symbol, ExclamationMark.symbol, +]); /** * A decoder that can decode a stream of `Line`s into a stream diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/angleBrackets.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/angleBrackets.ts new file mode 100644 index 00000000000..70d264bdd99 --- /dev/null +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/angleBrackets.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { Position } from '../../../core/position.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * A token that represent a `<` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class LeftAngleBracket extends BaseToken { + /** + * The underlying symbol of the token. + */ + public static readonly symbol: string = '<'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return LeftAngleBracket.symbol; + } + + /** + * Create new `LeftBracket` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): LeftAngleBracket { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new LeftAngleBracket(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `left-angle-bracket${this.range}`; + } +} + +/** + * A token that represent a `>` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class RightAngleBracket extends BaseToken { + /** + * The underlying symbol of the token. + */ + public static readonly symbol: string = '>'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return RightAngleBracket.symbol; + } + + /** + * Create new `RightAngleBracket` token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): RightAngleBracket { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new RightAngleBracket(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `right-angle-bracket${this.range}`; + } +} + +/** + * General angle bracket token type. + */ +export type TAngleBracket = LeftAngleBracket | RightAngleBracket; diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts index 5c6c1e46a5d..16165cf64a7 100644 --- a/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/brackets.ts @@ -36,7 +36,6 @@ export class LeftBracket extends BaseToken { const { range } = line; const startPosition = new Position(range.startLineNumber, atColumnNumber); - // the tab token length is 1, hence `+ 1` const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); return new LeftBracket(Range.fromPositions( @@ -81,7 +80,6 @@ export class RightBracket extends BaseToken { const { range } = line; const startPosition = new Position(range.startLineNumber, atColumnNumber); - // the tab token length is 1, hence `+ 1` const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); return new RightBracket(Range.fromPositions( @@ -97,3 +95,8 @@ export class RightBracket extends BaseToken { return `right-bracket${this.range}`; } } + +/** + * General bracket token type. + */ +export type TBracket = LeftBracket | RightBracket; diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts index 2c4b89d9ce5..76e9f0cd2b4 100644 --- a/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/colon.ts @@ -14,7 +14,7 @@ import { Line } from '../../linesCodec/tokens/line.js'; */ export class Colon extends BaseToken { /** - * The underlying symbol of the `LeftBracket` token. + * The underlying symbol of the token. */ public static readonly symbol: string = ':'; @@ -26,7 +26,7 @@ export class Colon extends BaseToken { } /** - * Create new `LeftBracket` token with range inside + * Create new token with range inside * the given `Line` at the given `column number`. */ public static newOnLine( @@ -36,7 +36,6 @@ export class Colon extends BaseToken { const { range } = line; const startPosition = new Position(range.startLineNumber, atColumnNumber); - // the tab token length is 1, hence `+ 1` const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); return new Colon(Range.fromPositions( diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/dash.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/dash.ts new file mode 100644 index 00000000000..ebc0179eeef --- /dev/null +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/dash.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { Position } from '../../../core/position.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * A token that represent a `-` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class Dash extends BaseToken { + /** + * The underlying symbol of the token. + */ + public static readonly symbol: string = '-'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return Dash.symbol; + } + + /** + * Create new token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): Dash { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new Dash(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `dash${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/exclamationMark.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/exclamationMark.ts new file mode 100644 index 00000000000..025edf70291 --- /dev/null +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/exclamationMark.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseToken } from '../../baseToken.js'; +import { Range } from '../../../core/range.js'; +import { Position } from '../../../core/position.js'; +import { Line } from '../../linesCodec/tokens/line.js'; + +/** + * A token that represent a `!` with a `range`. The `range` + * value reflects the position of the token in the original data. + */ +export class ExclamationMark extends BaseToken { + /** + * The underlying symbol of the token. + */ + public static readonly symbol: string = '!'; + + /** + * Return text representation of the token. + */ + public get text(): string { + return ExclamationMark.symbol; + } + + /** + * Create new token with range inside + * the given `Line` at the given `column number`. + */ + public static newOnLine( + line: Line, + atColumnNumber: number, + ): ExclamationMark { + const { range } = line; + + const startPosition = new Position(range.startLineNumber, atColumnNumber); + const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); + + return new ExclamationMark(Range.fromPositions( + startPosition, + endPosition, + )); + } + + /** + * Returns a string representation of the token. + */ + public override toString(): string { + return `exclamation-mark${this.range}`; + } +} diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts index 372e0b2ee3d..ddca12a2279 100644 --- a/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/hash.ts @@ -14,7 +14,7 @@ import { Line } from '../../linesCodec/tokens/line.js'; */ export class Hash extends BaseToken { /** - * The underlying symbol of the `LeftBracket` token. + * The underlying symbol of the token. */ public static readonly symbol: string = '#'; @@ -26,7 +26,7 @@ export class Hash extends BaseToken { } /** - * Create new `LeftBracket` token with range inside + * Create new token with range inside * the given `Line` at the given `column number`. */ public static newOnLine( @@ -36,7 +36,6 @@ export class Hash extends BaseToken { const { range } = line; const startPosition = new Position(range.startLineNumber, atColumnNumber); - // the tab token length is 1, hence `+ 1` const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); return new Hash(Range.fromPositions( diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts index b67f4e10f5c..d3509824f53 100644 --- a/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/parentheses.ts @@ -14,7 +14,7 @@ import { Line } from '../../linesCodec/tokens/line.js'; */ export class LeftParenthesis extends BaseToken { /** - * The underlying symbol of the `LeftParenthesis` token. + * The underlying symbol of the token. */ public static readonly symbol: string = '('; @@ -36,7 +36,6 @@ export class LeftParenthesis extends BaseToken { const { range } = line; const startPosition = new Position(range.startLineNumber, atColumnNumber); - // the tab token length is 1, hence `+ 1` const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); return new LeftParenthesis(Range.fromPositions( @@ -59,7 +58,7 @@ export class LeftParenthesis extends BaseToken { */ export class RightParenthesis extends BaseToken { /** - * The underlying symbol of the `RightParenthesis` token. + * The underlying symbol of the token. */ public static readonly symbol: string = ')'; @@ -81,7 +80,6 @@ export class RightParenthesis extends BaseToken { const { range } = line; const startPosition = new Position(range.startLineNumber, atColumnNumber); - // the tab token length is 1, hence `+ 1` const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); return new RightParenthesis(Range.fromPositions( @@ -97,3 +95,8 @@ export class RightParenthesis extends BaseToken { return `right-parenthesis${this.range}`; } } + +/** + * General parenthesis token type. + */ +export type TParenthesis = LeftParenthesis | RightParenthesis; diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts index 7f511c2626b..c0d775ff8cd 100644 --- a/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/tab.ts @@ -14,7 +14,7 @@ import { Position } from '../../../../../editor/common/core/position.js'; */ export class Tab extends BaseToken { /** - * The underlying symbol of the `Tab` token. + * The underlying symbol of the token. */ public static readonly symbol: string = '\t'; @@ -26,7 +26,7 @@ export class Tab extends BaseToken { } /** - * Create new `Tab` token with range inside + * Create new token with range inside * the given `Line` at the given `column number`. */ public static newOnLine( @@ -36,7 +36,6 @@ export class Tab extends BaseToken { const { range } = line; const startPosition = new Position(range.startLineNumber, atColumnNumber); - // the tab token length is 1, hence `+ 1` const endPosition = new Position(range.startLineNumber, atColumnNumber + this.symbol.length); return new Tab(Range.fromPositions( diff --git a/src/vs/editor/common/codecs/simpleCodec/tokens/word.ts b/src/vs/editor/common/codecs/simpleCodec/tokens/word.ts index fc3cefa79be..2ca5598ac4b 100644 --- a/src/vs/editor/common/codecs/simpleCodec/tokens/word.ts +++ b/src/vs/editor/common/codecs/simpleCodec/tokens/word.ts @@ -67,6 +67,6 @@ export class Word extends BaseToken { * Returns a string representation of the token. */ public override toString(): string { - return `word("${this.text.slice(0, 8)}")${this.range}`; + return `word("${this.text}")${this.range}`; } } diff --git a/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index 38aedd4868e..c3613ee9725 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { IJSONSchemaSnippet } from '../../../base/common/jsonSchema.js'; import { diffEditorDefaultOptions } from './diffEditor.js'; import { editorOptionsRegistry } from './editorOptions.js'; import { EDITOR_MODEL_DEFAULTS } from '../core/textModelDefaults.js'; @@ -318,3 +319,15 @@ export function isDiffEditorConfigurationKey(key: string): boolean { const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration(editorConfiguration); + +export async function registerEditorFontConfigurations(getFontSnippets: () => Promise) { + const editorKeysWithFont = ['editor.fontFamily']; + const fontSnippets = await getFontSnippets(); + for (const key of editorKeysWithFont) { + if ( + editorConfiguration.properties && editorConfiguration.properties[key] + ) { + editorConfiguration.properties[key].defaultSnippets = fontSnippets; + } + } +} diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 3fdecc8717e..6388b3c386c 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -1944,7 +1944,7 @@ class EffectiveExperimentalEditContextEnabled extends ComputedEditorOption; - onDidCompleteFirstTokenization: Event<{ textModel: model.ITextModel }>; + onDidChangeBackgroundTokenization: Event<{ textModel: model.ITextModel }>; tokenizeEncodedInstrumented(lineNumber: number, textModel: model.ITextModel): { result: Uint32Array; captureTime: number; metadataTime: number } | undefined; } @@ -431,6 +431,43 @@ export namespace CompletionItemKinds { return codicon; } + /** + * @internal + */ + export function toLabel(kind: CompletionItemKind): string { + switch (kind) { + case CompletionItemKind.Method: return localize('suggestWidget.kind.method', 'Method'); + case CompletionItemKind.Function: return localize('suggestWidget.kind.function', 'Function'); + case CompletionItemKind.Constructor: return localize('suggestWidget.kind.constructor', 'Constructor'); + case CompletionItemKind.Field: return localize('suggestWidget.kind.field', 'Field'); + case CompletionItemKind.Variable: return localize('suggestWidget.kind.variable', 'Variable'); + case CompletionItemKind.Class: return localize('suggestWidget.kind.class', 'Class'); + case CompletionItemKind.Struct: return localize('suggestWidget.kind.struct', 'Struct'); + case CompletionItemKind.Interface: return localize('suggestWidget.kind.interface', 'Interface'); + case CompletionItemKind.Module: return localize('suggestWidget.kind.module', 'Module'); + case CompletionItemKind.Property: return localize('suggestWidget.kind.property', 'Property'); + case CompletionItemKind.Event: return localize('suggestWidget.kind.event', 'Event'); + case CompletionItemKind.Operator: return localize('suggestWidget.kind.operator', 'Operator'); + case CompletionItemKind.Unit: return localize('suggestWidget.kind.unit', 'Unit'); + case CompletionItemKind.Value: return localize('suggestWidget.kind.value', 'Value'); + case CompletionItemKind.Constant: return localize('suggestWidget.kind.constant', 'Constant'); + case CompletionItemKind.Enum: return localize('suggestWidget.kind.enum', 'Enum'); + case CompletionItemKind.EnumMember: return localize('suggestWidget.kind.enumMember', 'Enum Member'); + case CompletionItemKind.Keyword: return localize('suggestWidget.kind.keyword', 'Keyword'); + case CompletionItemKind.Text: return localize('suggestWidget.kind.text', 'Text'); + case CompletionItemKind.Color: return localize('suggestWidget.kind.color', 'Color'); + case CompletionItemKind.File: return localize('suggestWidget.kind.file', 'File'); + case CompletionItemKind.Reference: return localize('suggestWidget.kind.reference', 'Reference'); + case CompletionItemKind.Customcolor: return localize('suggestWidget.kind.customcolor', 'Custom Color'); + case CompletionItemKind.Folder: return localize('suggestWidget.kind.folder', 'Folder'); + case CompletionItemKind.TypeParameter: return localize('suggestWidget.kind.typeParameter', 'Type Parameter'); + case CompletionItemKind.User: return localize('suggestWidget.kind.user', 'User'); + case CompletionItemKind.Issue: return localize('suggestWidget.kind.issue', 'Issue'); + case CompletionItemKind.Snippet: return localize('suggestWidget.kind.snippet', 'Snippet'); + default: return ''; + } + } + const data = new Map(); data.set('method', CompletionItemKind.Method); data.set('function', CompletionItemKind.Function); diff --git a/src/vs/editor/common/languages/highlights/typescript.scm b/src/vs/editor/common/languages/highlights/typescript.scm index 18d45571ccf..7b789592f61 100644 --- a/src/vs/editor/common/languages/highlights/typescript.scm +++ b/src/vs/editor/common/languages/highlights/typescript.scm @@ -3,118 +3,202 @@ ; Variables -(identifier) @variable +(identifier) @variable.ts + +(_ + object: (identifier) @variable.other.object.ts) ; Literals -(this) @variable.language.this -(super) @variable.language.super +(this) @variable.language.this.ts +(super) @variable.language.super.ts -(comment) @comment +(comment) @comment.ts ; TODO: This doesn't seem to be working -(escape_sequence) @constant.character.escape +(escape_sequence) @constant.character.escape.ts -[ - (string) +((string) @string.quoted.single.ts + (#match? @string.quoted.single.ts "^'[^']*'$")) + +((string) @string.quoted.double.ts + (#match? @string.quoted.double.ts "^\"[^\"]*\"$")) + +([ (template_string) (template_literal_type) -] @string +] @string.template.ts) + +(string . + ([ + "\"" + "'" + ]) @punctuation.definition.string.begin.ts) + +(string + ([ + "\"" + "'" + ]) @punctuation.definition.string.end.ts .) + +(template_string . ("`") @punctuation.definition.string.template.begin.ts) + +(template_string ("`") @punctuation.definition.string.template.end.ts .) ; NOTE: the typescript grammar doesn't break regex into nice parts so as to capture parts of it separately -(regex) @string.regexp -(number) @constant.numeric +(regex) @string.regexp.ts +(number) @constant.numeric.ts ; Properties (member_expression object: (this) - property: (property_identifier) @variable) + property: (property_identifier) @variable.ts) (member_expression - property: (property_identifier) @variable.other.constant - (#match? @variable.other.constant "^[A-Z][A-Z_]+$")) + property: (property_identifier) @variable.other.constant.ts + (#match? @variable.other.constant.ts "^[A-Z][A-Z_]+$")) [ (property_identifier) (shorthand_property_identifier) - (shorthand_property_identifier_pattern)] @variable + (shorthand_property_identifier_pattern)] @variable.ts ; Function and method definitions (function_expression - name: (identifier) @entity.name.function) + name: (identifier) @entity.name.function.ts) +(function_signature + name: (identifier) @entity.name.function.ts) (function_declaration - name: (identifier) @entity.name.function) + name: (identifier) @entity.name.function.ts) (method_definition - name: (property_identifier) @meta.definition.method @entity.name.function - (#not-eq? @entity.name.function "constructor")) + name: (property_identifier) @meta.definition.method.ts @entity.name.function.ts + (#not-eq? @entity.name.function.ts "constructor")) (method_definition - name: (property_identifier) @meta.definition.method @storage.type - (#eq? @storage.type "constructor")) + name: (property_identifier) @meta.definition.method.ts @storage.type.ts + (#eq? @storage.type.ts "constructor")) (method_signature - name: (property_identifier) @meta.definition.method @entity.name.function) + name: (property_identifier) @meta.definition.method.ts @entity.name.function.ts) +(generator_function_declaration + "*" @keyword.generator.asterisk.ts) +(generator_function_declaration + name: (identifier) @meta.definition.function.ts @entity.name.function.ts) (pair - key: (property_identifier) @entity.name.function + key: (property_identifier) @entity.name.function.ts value: [(function_expression) (arrow_function)]) (assignment_expression left: (member_expression - property: (property_identifier) @entity.name.function) + property: (property_identifier) @entity.name.function.ts) right: [(function_expression) (arrow_function)]) (variable_declarator - name: (identifier) @entity.name.function + name: (identifier) @entity.name.function.ts value: [(function_expression) (arrow_function)]) (assignment_expression - left: (identifier) @entity.name.function + left: (identifier) @entity.name.function.ts right: [(function_expression) (arrow_function)]) (required_parameter - (identifier) @variable.parameter) + (identifier) @variable.parameter.ts) + +(required_parameter + (_ + ([ + (identifier) + (shorthand_property_identifier_pattern) + ]) @variable.parameter.ts)) (optional_parameter - (identifier) @variable.parameter) + (identifier) @variable.parameter.ts) + +(optional_parameter + (_ + ([ + (identifier) + (shorthand_property_identifier_pattern) + ]) @variable.parameter.ts)) + +(catch_clause + parameter: (identifier) @variable.parameter.ts) + +(index_signature + name: (identifier) @variable.parameter.ts) + +(arrow_function + parameter: (identifier) @variable.parameter.ts) ; Function and method calls (call_expression - function: (identifier) @entity.name.function) + function: (identifier) @entity.name.function.ts) (call_expression function: (member_expression - object: (identifier) @support.class.promise) - (#eq? @support.class.promise "Promise")) + object: (identifier) @support.class.promise.ts) + (#eq? @support.class.promise.ts "Promise")) (call_expression function: (member_expression - property: (property_identifier) @entity.name.function)) + property: (property_identifier) @entity.name.function.ts)) -(new_expression) @new.expr +(new_expression) @new.expr.ts (new_expression - constructor: (identifier) @entity.name.function) + constructor: (identifier) @entity.name.function.ts) ; Special identifiers -(predefined_type) @support.type -(predefined_type (["string" "boolean" "number" "any" "unknown"])) @support.type.primitive -(type_identifier) @entity.name.type +(predefined_type) @support.type.ts +(predefined_type (["string" "boolean" "number" "any" "unknown" "never" "void"])) @support.type.primitive.ts -([ - (identifier) - (shorthand_property_identifier) - (shorthand_property_identifier_pattern)] @variable.other.constant - (#match? @variable.other.constant "^[A-Z][A-Z_]+$")) +(_ + (type_identifier) @entity.name.type.ts) + +(type_annotation + ([ + (type_identifier) + (nested_type_identifier) + ]) @meta.type.annotation.ts @entity.name.type.ts) + +(class_declaration + (type_identifier) @entity.name.type.class.ts) + +(type_alias_declaration + (type_identifier) @entity.name.type.alias.ts) +(type_alias_declaration + value: (_ + (type_identifier) @entity.name.type.ts)) + +(interface_declaration + (type_identifier) @entity.name.type.interface.ts) + +(internal_module + name: (identifier) @entity.name.type.ts) + +(enum_declaration + name: (identifier) @entity.name.type.enum.ts) + +( + [ + (_ name: (identifier)) + (shorthand_property_identifier) + (shorthand_property_identifier_pattern) + ] @variable.other.constant.ts + (#match? @variable.other.constant.ts "^[A-Z][A-Z_]+$")) (extends_clause - value: (identifier) @entity.other.inherited-class) + value: (identifier) @entity.other.inherited-class.ts) + +(extends_type_clause + type: (type_identifier) @entity.other.inherited-class.ts) (implements_clause - (type_identifier) @entity.other.inherited-class) + (type_identifier) @entity.other.inherited-class.ts) ; Tokens @@ -125,7 +209,7 @@ "," ":" "?" -] @punctuation.delimiter +] @punctuation.delimiter.ts [ "!" @@ -135,7 +219,7 @@ "&&" "||" "??" -] @keyword.operator.logical +] @keyword.operator.logical.ts (binary_expression ([ "-" @@ -144,18 +228,22 @@ "/" "%" "^" -]) @keyword.operator.arithmetic) +]) @keyword.operator.arithmetic.ts) (binary_expression ([ "<" "<=" ">" ">=" -]) @keyword.operator.relational) +]) @keyword.operator.relational.ts) + +(unary_expression ([ + "-" +]) @keyword.operator.arithmetic.ts) [ "=" -] @keyword.operator.assignment +] @keyword.operator.assignment.ts (augmented_assignment_expression ([ "-=" @@ -169,15 +257,15 @@ "&&=" "||=" "??=" -]) @keyword.operator.assignment.compound) +]) @keyword.operator.assignment.compound.ts) [ "++" -] @keyword.operator.increment +] @keyword.operator.increment.ts [ "--" -] @keyword.operator.decrement +] @keyword.operator.decrement.ts [ "**" @@ -193,16 +281,28 @@ "~" "&" "|" -] @keyword.operator +] @keyword.operator.ts (union_type - ("|") @keyword.operator.type) + ("|") @keyword.operator.type.ts) (intersection_type - ("&") @keyword.operator.type) + ("&") @keyword.operator.type.ts) (type_annotation - (":") @keyword.operator.type.annotation) + (":") @keyword.operator.type.annotation.ts) + +(index_signature + (":") @keyword.operator.type.annotation.ts) + +(type_predicate_annotation + (":") @keyword.operator.type.annotation.ts) + +(conditional_type + ([ + "?" + ":" + ]) @keyword.operator.ternary.ts) [ "{" @@ -211,36 +311,40 @@ ")" "[" "]" -] @punctuation +] @punctuation.ts (template_substitution - "${" @punctuation.definition.template-expression.begin - "}" @punctuation.definition.template-expression.end) + "${" @punctuation.definition.template-expression.begin.ts + "}" @punctuation.definition.template-expression.end.ts) (template_type - "${" @punctuation.definition.template-expression.begin - "}" @punctuation.definition.template-expression.end) + "${" @punctuation.definition.template-expression.begin.ts + "}" @punctuation.definition.template-expression.end.ts) (type_arguments - "<" @punctuation.definition.typeparameters - ">" @punctuation.definition.typeparameters) + "<" @punctuation.definition.typeparameters.begin.ts + ">" @punctuation.definition.typeparameters.end.ts) + +(type_parameters + "<" @punctuation.definition.typeparameters.begin.ts + ">" @punctuation.definition.typeparameters.end.ts) ; Keywords -("typeof") @keyword.operator.expression.typeof +("typeof") @keyword.operator.expression.typeof.ts -(binary_expression "instanceof" @keyword.operator.expression.instanceof) +(binary_expression "instanceof" @keyword.operator.expression.instanceof.ts) -("of") @keyword.operator.expression.of +("of") @keyword.operator.expression.of.ts -("is") @keyword.operator.expression.is +("is") @keyword.operator.expression.is.ts [ "delete" "in" "infer" "keyof" -] @keyword.operator.expression +] @keyword.operator.expression.ts [ "as" @@ -264,10 +368,9 @@ "switch" "throw" "try" - "type" "while" "yield" -] @keyword.control +] @keyword.control.ts [ "abstract" @@ -281,7 +384,7 @@ "public" "readonly" "static" -] @storage.modifier +] @storage.modifier.ts [ "=>" @@ -295,73 +398,84 @@ "namespace" "set" "var" -] @storage.type +] @storage.type.ts + +("type") @storage.type.type.ts + +[ + "module" +] @storage.type.namespace.ts [ "debugger" "target" "with" -] @keyword +] @keyword.ts -(regex_flags) @keyword +(regex_flags) @keyword.ts -[ - "void" -] @support.type.primitive +(unary_expression + "void" @keyword.operator.expression.void.ts) [ "new" -] @keyword.operator.new +] @keyword.operator.new.ts (public_field_definition - ("?") @keyword.operator.optional) + ("?") @keyword.operator.optional.ts) (property_signature - ("?") @keyword.operator.optional) + ("?") @keyword.operator.optional.ts) + +(method_signature + ("?") @keyword.operator.optional.ts) (optional_parameter ([ "?" ":" - ]) @keyword.operator.optional) + ]) @keyword.operator.optional.ts) (ternary_expression ([ "?" ":" - ]) @keyword.operator.ternary) + ]) @keyword.operator.ternary.ts) (optional_chain - ("?.") @punctuation.accessor.optional) + ("?.") @punctuation.accessor.optional.ts) -(rest_pattern) @keyword.operator.rest +(rest_pattern + ("...") @keyword.operator.rest.ts) +(rest_type + ("...") @keyword.operator.rest.ts) (spread_element - ("...") @keyword.operator.spread) + ("...") @keyword.operator.spread.ts) ; Language constants [ (null) -] @constant.language.null +] @constant.language.null.ts [ (undefined) -] @constant.language.undefined +] @constant.language.undefined.ts - ((identifier) @constant.language.nan - (#eq? @constant.language.nan "NaN")) +((identifier) @constant.language.nan.ts + (#eq? @constant.language.nan.ts "NaN")) - ((identifier) @constant.language.infinity - (#eq? @constant.language.infinity "Infinity")) +((identifier) @constant.language.infinity.ts + (#eq? @constant.language.infinity.ts "Infinity")) [ (true) -] @constant.language.boolean.true +] @constant.language.boolean.true.ts [ (false) -] @constant.language.boolean.false +] @constant.language.boolean.false.ts (literal_type [ @@ -369,7 +483,7 @@ (undefined) (true) (false) - ] @support.type.builtin) + ] @support.type.builtin.ts) (namespace_import - "*" @constant.language) + "*" @constant.language.ts) diff --git a/src/vs/editor/common/model/tokenStore.ts b/src/vs/editor/common/model/tokenStore.ts index 49264b40ae6..c180e1b15a3 100644 --- a/src/vs/editor/common/model/tokenStore.ts +++ b/src/vs/editor/common/model/tokenStore.ts @@ -35,7 +35,9 @@ class ListNode implements IDisposable { this._length += node.length; this._updateParentLength(node.length); - node.parent = this; + if (!isLeaf(node)) { + node.parent = this; + } } private _updateParentLength(delta: number) { @@ -61,7 +63,9 @@ class ListNode implements IDisposable { this._length += node.length; this._updateParentLength(node.length); - node.parent = this; + if (!isLeaf(node)) { + node.parent = this; + } } unprependChild(): Node { @@ -91,7 +95,6 @@ type Node = ListNode | LeafNode; interface LeafNode { readonly length: number; - parent?: ListNode; token: number; tokenQuality: TokenQuality; height: 0; @@ -270,7 +273,9 @@ export class TokenStore implements IDisposable { const currentOffset = node.offset; if (currentOffset < updateOffsetStart && currentOffset + node.node.length <= updateOffsetStart) { - node.node.parent = undefined; + if (!isLeaf(node.node)) { + node.node.parent = undefined; + } precedingNodes.push(node.node); continue; } else if (isLeaf(node.node) && (currentOffset < updateOffsetStart)) { @@ -284,7 +289,9 @@ export class TokenStore implements IDisposable { } if (currentOffset >= firstUnchangedOffsetAfterUpdate) { - node.node.parent = undefined; + if (!isLeaf(node.node)) { + node.node.parent = undefined; + } postcedingNodes.push(node.node); continue; } else if (isLeaf(node.node) && (currentOffset + node.node.length >= firstUnchangedOffsetAfterUpdate)) { @@ -492,7 +499,7 @@ export class TokenStore implements IDisposable { while (stack.length > 0) { const [node, visited] = stack.pop()!; if (isLeaf(node)) { - node.parent = undefined; + // leaf node does not need to be disposed } else if (!visited) { stack.push([node, true]); for (let i = node.children.length - 1; i >= 0; i--) { diff --git a/src/vs/editor/common/model/treeSitterTokenStoreService.ts b/src/vs/editor/common/model/treeSitterTokenStoreService.ts index 8c544cdb416..c304ad9309c 100644 --- a/src/vs/editor/common/model/treeSitterTokenStoreService.ts +++ b/src/vs/editor/common/model/treeSitterTokenStoreService.ts @@ -9,16 +9,19 @@ import { TokenQuality, TokenStore, TokenUpdate } from './tokenStore.js'; import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; +import { IModelContentChangedEvent } from '../textModelEvents.js'; export interface ITreeSitterTokenizationStoreService { readonly _serviceBrand: undefined; setTokens(model: ITextModel, tokens: TokenUpdate[], tokenQuality: TokenQuality): void; + handleContentChanged(model: ITextModel, e: IModelContentChangedEvent): void; getTokens(model: ITextModel, line: number): Uint32Array | undefined; updateTokens(model: ITextModel, version: number, updates: { oldRangeLength?: number; newTokens: TokenUpdate[] }[], tokenQuality: TokenQuality): void; markForRefresh(model: ITextModel, range: Range): void; getNeedsRefresh(model: ITextModel): { range: Range; startOffset: number; endOffset: number }[]; hasTokens(model: ITextModel, accurateForRange?: Range): boolean; rangeHasTokens(model: ITextModel, range: Range, minimumTokenQuality: TokenQuality): boolean; + delete(model: ITextModel): void; } export const ITreeSitterTokenizationStoreService = createDecorator('treeSitterTokenizationStoreService'); @@ -41,37 +44,6 @@ class TreeSitterTokenizationStoreService implements ITreeSitterTokenizationStore this.tokens.set(model, { store: store, accurateVersion: model.getVersionId(), disposables, guessVersion: model.getVersionId() }); store.buildStore(tokens, tokenQuality); - disposables.add(model.onDidChangeContent(e => { - const storeInfo = this.tokens.get(model); - if (!storeInfo) { - return; - } - - storeInfo.guessVersion = e.versionId; - for (const change of e.changes) { - if (change.text.length > change.rangeLength) { - // If possible, use the token before the change as the starting point for the new token. - // This is more likely to let the new text be the correct color as typeing is usually at the end of the token. - const offset = change.rangeOffset > 0 ? change.rangeOffset - 1 : change.rangeOffset; - const oldToken = storeInfo.store.getTokenAt(offset); - let newToken: TokenUpdate; - if (oldToken) { - // Insert. Just grow the token at this position to include the insert. - newToken = { startOffsetInclusive: oldToken.startOffsetInclusive, length: oldToken.length + change.text.length - change.rangeLength, token: oldToken.token }; - } else { - // The document got larger and the change is at the end of the document. - newToken = { startOffsetInclusive: offset, length: change.text.length + 1, token: 0 }; - } - storeInfo.store.update(oldToken?.length ?? 0, [newToken], TokenQuality.EditGuess); - } else if (change.text.length < change.rangeLength) { - // Delete. Delete the tokens at the corresponding range. - const deletedCharCount = change.rangeLength - change.text.length; - storeInfo.store.delete(deletedCharCount, change.rangeOffset); - } - const refreshLength = change.rangeLength > change.text.length ? change.rangeLength : change.text.length; - storeInfo.store.markForRefresh(change.rangeOffset, change.rangeOffset + refreshLength); - } - })); disposables.add(model.onWillDispose(() => { const storeInfo = this.tokens.get(model); if (storeInfo) { @@ -81,6 +53,39 @@ class TreeSitterTokenizationStoreService implements ITreeSitterTokenizationStore })); } + handleContentChanged(model: ITextModel, e: IModelContentChangedEvent): void { + const storeInfo = this.tokens.get(model); + if (!storeInfo) { + return; + } + + storeInfo.guessVersion = e.versionId; + for (const change of e.changes) { + if (change.text.length > change.rangeLength) { + // If possible, use the token before the change as the starting point for the new token. + // This is more likely to let the new text be the correct color as typeing is usually at the end of the token. + const offset = change.rangeOffset > 0 ? change.rangeOffset - 1 : change.rangeOffset; + const oldToken = storeInfo.store.getTokenAt(offset); + let newToken: TokenUpdate; + if (oldToken) { + // Insert. Just grow the token at this position to include the insert. + newToken = { startOffsetInclusive: oldToken.startOffsetInclusive, length: oldToken.length + change.text.length - change.rangeLength, token: oldToken.token }; + } else { + // The document got larger and the change is at the end of the document. + newToken = { startOffsetInclusive: offset, length: change.text.length, token: 0 }; + } + storeInfo.store.update(oldToken?.length ?? 0, [newToken], TokenQuality.EditGuess); + } else if (change.text.length < change.rangeLength) { + // Delete. Delete the tokens at the corresponding range. + const deletedCharCount = change.rangeLength - change.text.length; + storeInfo.store.delete(deletedCharCount, change.rangeOffset); + } + const refreshLength = change.rangeLength > change.text.length ? change.rangeLength : change.text.length; + storeInfo.store.markForRefresh(change.rangeOffset, change.rangeOffset + refreshLength); + } + + } + rangeHasTokens(model: ITextModel, range: Range, minimumTokenQuality: TokenQuality): boolean { const tokens = this.tokens.get(model); if (!tokens) { @@ -158,6 +163,14 @@ class TreeSitterTokenizationStoreService implements ITreeSitterTokenizationStore })); } + delete(model: ITextModel): void { + const storeInfo = this.tokens.get(model); + if (storeInfo) { + storeInfo.disposables.dispose(); + this.tokens.delete(model); + } + } + dispose(): void { for (const [, value] of this.tokens) { value.disposables.dispose(); diff --git a/src/vs/editor/common/model/treeSitterTokens.ts b/src/vs/editor/common/model/treeSitterTokens.ts index 105a81b2f07..566de4466fb 100644 --- a/src/vs/editor/common/model/treeSitterTokens.ts +++ b/src/vs/editor/common/model/treeSitterTokens.ts @@ -24,7 +24,7 @@ export class TreeSitterTokens extends AbstractTokens { private _lastLanguageId: string | undefined; private readonly _tokensChangedListener: MutableDisposable = this._register(new MutableDisposable()); - private readonly _firstTokenizationCompleteListener: MutableDisposable = this._register(new MutableDisposable()); + private readonly _onDidChangeBackgroundTokenization: MutableDisposable = this._register(new MutableDisposable()); constructor(languageIdCodec: ILanguageIdCodec, textModel: TextModel, @@ -45,11 +45,10 @@ export class TreeSitterTokens extends AbstractTokens { this._onDidChangeTokens.fire(e.changes); } }); - this._firstTokenizationCompleteListener.value = this._tokenizationSupport?.onDidCompleteFirstTokenization(e => { + this._onDidChangeBackgroundTokenization.value = this._tokenizationSupport?.onDidChangeBackgroundTokenization(e => { if (e.textModel === this._textModel) { this._backgroundTokenizationState = BackgroundTokenizationState.Completed; this._onDidChangeBackgroundTokenizationState.fire(); - this._firstTokenizationCompleteListener.clear(); } }); } @@ -57,7 +56,7 @@ export class TreeSitterTokens extends AbstractTokens { public getLineTokens(lineNumber: number): LineTokens { const content = this._textModel.getLineContent(lineNumber); - if (this._tokenizationSupport) { + if (this._tokenizationSupport && content.length > 0) { const rawTokens = this._tokenStore.getTokens(this._textModel, lineNumber); if (rawTokens) { return new LineTokens(rawTokens, content, this._languageIdCodec); @@ -89,6 +88,8 @@ export class TreeSitterTokens extends AbstractTokens { if (e.isFlush) { // Don't fire the event, as the view might not have got the text change event yet this.resetTokenization(false); + } else { + this._tokenStore.handleContentChanged(this._textModel, e); } } diff --git a/src/vs/editor/common/services/treeSitter/cursorUtils.ts b/src/vs/editor/common/services/treeSitter/cursorUtils.ts new file mode 100644 index 00000000000..b5b6e701dd3 --- /dev/null +++ b/src/vs/editor/common/services/treeSitter/cursorUtils.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Parser from '@vscode/tree-sitter-wasm'; + +export function gotoNextSibling(newCursor: Parser.TreeCursor, oldCursor: Parser.TreeCursor) { + const n = newCursor.gotoNextSibling(); + const o = oldCursor.gotoNextSibling(); + if (n !== o) { + throw new Error('Trees are out of sync'); + } + return n && o; +} + +export function gotoParent(newCursor: Parser.TreeCursor, oldCursor: Parser.TreeCursor) { + const n = newCursor.gotoParent(); + const o = oldCursor.gotoParent(); + if (n !== o) { + throw new Error('Trees are out of sync'); + } + return n && o; +} + +export function gotoNthChild(newCursor: Parser.TreeCursor, oldCursor: Parser.TreeCursor, index: number) { + const n = newCursor.gotoFirstChild(); + const o = oldCursor.gotoFirstChild(); + if (n !== o) { + throw new Error('Trees are out of sync'); + } + if (index === 0) { + return n && o; + } + for (let i = 1; i <= index; i++) { + const nn = newCursor.gotoNextSibling(); + const oo = oldCursor.gotoNextSibling(); + if (nn !== oo) { + throw new Error('Trees are out of sync'); + } + if (!nn || !oo) { + return false; + } + } + return n && o; +} + +export function nextSiblingOrParentSibling(newCursor: Parser.TreeCursor, oldCursor: Parser.TreeCursor) { + do { + if (newCursor.currentNode.nextSibling) { + return gotoNextSibling(newCursor, oldCursor); + } + if (newCursor.currentNode.parent) { + gotoParent(newCursor, oldCursor); + } + } while (newCursor.currentNode.nextSibling || newCursor.currentNode.parent); + return false; +} + +export function getClosestPreviousNodes(cursor: Parser.TreeCursor, tree: Parser.Tree): Parser.Node | undefined { + // Go up parents until the end of the parent is before the start of the current. + const findPrev = tree.walk(); + findPrev.resetTo(cursor); + + const startingNode = cursor.currentNode; + do { + if (findPrev.currentNode.previousSibling && ((findPrev.currentNode.endIndex - findPrev.currentNode.startIndex) !== 0)) { + findPrev.gotoPreviousSibling(); + } else { + while (!findPrev.currentNode.previousSibling && findPrev.currentNode.parent) { + findPrev.gotoParent(); + } + findPrev.gotoPreviousSibling(); + } + } while ((findPrev.currentNode.endIndex > startingNode.startIndex) + && (findPrev.currentNode.parent || findPrev.currentNode.previousSibling) + + && (findPrev.currentNode.id !== startingNode.id)); + + if ((findPrev.currentNode.id !== startingNode.id) && findPrev.currentNode.endIndex <= startingNode.startIndex) { + return findPrev.currentNode; + } else { + return undefined; + } +} diff --git a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts index 2df635ed6d9..985207918cd 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterParserService.ts @@ -22,9 +22,9 @@ import { IEnvironmentService } from '../../../../platform/environment/common/env import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; import { PromiseResult } from '../../../../base/common/observable.js'; import { Range } from '../../core/range.js'; -import { Position } from '../../core/position.js'; import { LimitedQueue } from '../../../../base/common/async.js'; import { TextLength } from '../../core/textLength.js'; +import { getClosestPreviousNodes, gotoNthChild, gotoParent, nextSiblingOrParentSibling } from './cursorUtils.js'; const EDITOR_TREESITTER_TELEMETRY = 'editor.experimental.treeSitterTelemetry'; const MODULE_LOCATION_SUBPATH = `@vscode/tree-sitter-wasm/wasm`; @@ -88,7 +88,7 @@ export class TextModelTreeSitter extends Disposable implements ITextModelTreeSit const treeSitterTree = this._parseSessionDisposables.add(new TreeSitterParseResult(new Parser(), language, this._logService, this._telemetryService)); this._parseResult = treeSitterTree; this._parseSessionDisposables.add(treeSitterTree.onDidUpdate(e => { - if (e.ranges && (e.versionId > this._versionId)) { + if (e.ranges && (e.versionId >= this._versionId)) { this._versionId = e.versionId; this._onDidChangeParseResult.fire({ ranges: e.ranges, versionId: e.versionId }); } @@ -133,16 +133,6 @@ const enum TelemetryParseType { Incremental = 'incrementalParse' } -interface ChangedRange { - newNodeId: number; - newStartPosition: Position; - newEndPosition: Position; - newStartIndex: number; - newEndIndex: number; - oldStartIndex: number; - oldEndIndex: number; -} - export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResult { private _tree: Parser.Tree | undefined; private _lastFullyParsed: Parser.Tree | undefined; @@ -172,91 +162,13 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul get tree() { return this._lastFullyParsed; } get isDisposed() { return this._isDisposed; } - private findChangedNodes(newTree: Parser.Tree, oldTree: Parser.Tree): ChangedRange[] { + private findChangedNodes(newTree: Parser.Tree, oldTree: Parser.Tree): Parser.Node[] { const newCursor = newTree.walk(); const oldCursor = oldTree.walk(); - const gotoNextSibling = () => { - const n = newCursor.gotoNextSibling(); - const o = oldCursor.gotoNextSibling(); - if (n !== o) { - throw new Error('Trees are out of sync'); - } - return n && o; - }; - const gotoParent = () => { - const n = newCursor.gotoParent(); - const o = oldCursor.gotoParent(); - if (n !== o) { - throw new Error('Trees are out of sync'); - } - return n && o; - }; - const gotoNthChild = (index: number) => { - const n = newCursor.gotoFirstChild(); - const o = oldCursor.gotoFirstChild(); - if (n !== o) { - throw new Error('Trees are out of sync'); - } - if (index === 0) { - return n && o; - } - for (let i = 1; i <= index; i++) { - const nn = newCursor.gotoNextSibling(); - const oo = oldCursor.gotoNextSibling(); - if (nn !== oo) { - throw new Error('Trees are out of sync'); - } - if (!nn || !oo) { - return false; - } - } - return n && o; - }; - const changedRanges: ChangedRange[] = []; + const nodes: Parser.Node[] = []; let next = true; - const nextSiblingOrParentSibling = () => { - do { - if (newCursor.currentNode.nextSibling) { - return gotoNextSibling(); - } - if (newCursor.currentNode.parent) { - gotoParent(); - } - } while (newCursor.currentNode.nextSibling || newCursor.currentNode.parent); - return false; - }; - const getClosestPreviousNodes = (): { old: Parser.Node; new: Parser.Node } | undefined => { - // Go up parents until the end of the parent is before the start of the current. - const newFindPrev = newTree.walk(); - newFindPrev.resetTo(newCursor); - const oldFindPrev = oldTree.walk(); - oldFindPrev.resetTo(oldCursor); - const startingNode = newCursor.currentNode; - do { - if (newFindPrev.currentNode.previousSibling && ((newFindPrev.currentNode.endIndex - newFindPrev.currentNode.startIndex) !== 0)) { - newFindPrev.gotoPreviousSibling(); - oldFindPrev.gotoPreviousSibling(); - } else { - while (!newFindPrev.currentNode.previousSibling && newFindPrev.currentNode.parent) { - newFindPrev.gotoParent(); - oldFindPrev.gotoParent(); - } - newFindPrev.gotoPreviousSibling(); - oldFindPrev.gotoPreviousSibling(); - } - } while ((newFindPrev.currentNode.endIndex > startingNode.startIndex) - && (newFindPrev.currentNode.parent || newFindPrev.currentNode.previousSibling) - - && (newFindPrev.currentNode.id !== startingNode.id)); - - if ((newFindPrev.currentNode.id !== startingNode.id) && newFindPrev.currentNode.endIndex <= startingNode.startIndex) { - return { old: oldFindPrev.currentNode, new: newFindPrev.currentNode }; - } else { - return undefined; - } - }; do { if (newCursor.currentNode.hasChanges) { // Check if only one of the children has changes. @@ -272,71 +184,110 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul return c?.hasChanges; }); // If we have changes and we *had* an error, the whole node should be refreshed. - if ((changedChildren.length === 0) || oldCursor.currentNode.hasError) { + if ((changedChildren.length === 0)) { // walk up again until we get to the first one that's named as unnamed nodes can be too granular while (newCursor.currentNode.parent && !newCursor.currentNode.isNamed && next) { - next = gotoParent(); + next = gotoParent(newCursor, oldCursor); } const newNode = newCursor.currentNode; - const oldNode = oldCursor.currentNode; - - const newEndPosition = new Position(newNode.endPosition.row + 1, newNode.endPosition.column + 1); - const oldEndIndex = oldNode.endIndex; - - // Fill holes between nodes. - const closestPrev = getClosestPreviousNodes(); - const newStartPosition = new Position(closestPrev ? closestPrev.new.endPosition.row + 1 : newNode.startPosition.row + 1, closestPrev ? closestPrev.new.endPosition.column + 1 : newNode.startPosition.column + 1); - const newStartIndex = closestPrev ? closestPrev.new.endIndex : newNode.startIndex; - const oldStartIndex = closestPrev ? closestPrev.old.endIndex : oldNode.startIndex; - - changedRanges.push({ newStartPosition, newEndPosition, oldStartIndex, oldEndIndex, newNodeId: newNode.id, newStartIndex, newEndIndex: newNode.endIndex }); - next = nextSiblingOrParentSibling(); + nodes.push(newNode); + next = nextSiblingOrParentSibling(newCursor, oldCursor); } else if (changedChildren.length >= 1) { - next = gotoNthChild(indexChangedChildren[0]); + next = gotoNthChild(newCursor, oldCursor, indexChangedChildren[0]); } } else { - next = nextSiblingOrParentSibling(); + next = nextSiblingOrParentSibling(newCursor, oldCursor); } } while (next); - if (changedRanges.length === 0 && newTree.rootNode.hasChanges) { - return [{ newStartPosition: new Position(newTree.rootNode.startPosition.row + 1, newTree.rootNode.startPosition.column + 1), newEndPosition: new Position(newTree.rootNode.endPosition.row + 1, newTree.rootNode.endPosition.column + 1), oldStartIndex: oldTree.rootNode.startIndex, oldEndIndex: oldTree.rootNode.endIndex, newStartIndex: newTree.rootNode.startIndex, newEndIndex: newTree.rootNode.endIndex, newNodeId: newTree.rootNode.id }]; - } else { - return changedRanges; - } + return nodes; } - private calculateRangeChange(model: ITextModel, changedNodes: ChangedRange[] | undefined): RangeChange[] | undefined { - if (!changedNodes) { - return undefined; - } + private findTreeChanges(newTree: Parser.Tree, changedNodes: Parser.Node[]): RangeChange[] { + const mergedChanges: RangeChange[] = []; - // Collapse conginguous ranges - const ranges: RangeChange[] = []; - for (let i = 0; i < changedNodes.length; i++) { - const node = changedNodes[i]; + // Find the parent in the new tree of the changed node + for (let nodeIndex = 0; nodeIndex < changedNodes.length; nodeIndex++) { + const node = changedNodes[nodeIndex]; - // Check if contiguous with previous - const prevNode = changedNodes[i - 1]; - if ((i > 0) && prevNode.newEndPosition.equals(node.newStartPosition)) { - const prevRangeChange = ranges[ranges.length - 1]; - prevRangeChange.newRange = new Range(prevRangeChange.newRange.startLineNumber, prevRangeChange.newRange.startColumn, node.newEndPosition.lineNumber, node.newEndPosition.column); - prevRangeChange.oldRangeLength = node.oldEndIndex - prevNode.oldStartIndex; - prevRangeChange.newRangeEndOffset = node.newEndIndex; + if (mergedChanges.length > 0) { + if ((node.startIndex > mergedChanges[mergedChanges.length - 1].newRangeStartOffset) && (node.endIndex < mergedChanges[mergedChanges.length - 1].newRangeEndOffset)) { + // This node is within the previous range, skip it + continue; + } + } + + const cursor = newTree.walk(); + const cursorContainersNode = () => cursor.startIndex <= node.startIndex && cursor.endIndex >= node.endIndex; + + while (cursorContainersNode()) { + // See if we can go to a child + let child = cursor.gotoFirstChild(); + let foundChild = false; + while (child) { + if (cursorContainersNode()) { + foundChild = true; + break; + } else { + child = cursor.gotoNextSibling(); + } + } + if (!foundChild) { + cursor.gotoParent(); + break; + } + if (cursor.currentNode.childCount === 0) { + break; + } + } + + let nodesInRange: Parser.Node[]; + // It's possible we end up with a really large range if the parent node is big + // Try to avoid this large range by finding several smaller nodes that together encompass the range of the changed node. + const foundNodeSize = cursor.endIndex - cursor.startIndex; + if (foundNodeSize > 5000) { + // Try to find 3 consecutive nodes that together encompass the changed node. + let child = cursor.gotoFirstChild(); + nodesInRange = []; + while (child) { + if (cursor.startIndex <= node.startIndex && cursor.endIndex > node.startIndex) { + // Found the starting point of our nodes + nodesInRange.push(cursor.currentNode); + do { + child = cursor.gotoNextSibling(); + } while (child && (cursor.endIndex < node.endIndex)); + + nodesInRange.push(cursor.currentNode); + break; + } + child = cursor.gotoNextSibling(); + } } else { - ranges.push({ newRange: Range.fromPositions(node.newStartPosition, node.newEndPosition), oldRangeLength: node.oldEndIndex - node.oldStartIndex, newRangeStartOffset: node.newStartIndex, newRangeEndOffset: node.newEndIndex }); + nodesInRange = [cursor.currentNode]; } - } - if (ranges.length > 0) { - const lastRange = ranges[ranges.length - 1]; - const maxLine = model.getLineCount(); - if (lastRange.newRange.endLineNumber > maxLine) { - lastRange.newRange = new Range(lastRange.newRange.startLineNumber, lastRange.newRange.startColumn, maxLine, model.getLineMaxColumn(maxLine)); + // Fill in gaps between nodes + // Reset the cursor to the first node in the range; + while (cursor.currentNode.id !== nodesInRange[0].id) { + cursor.gotoPreviousSibling(); + } + const previousNode = getClosestPreviousNodes(cursor, newTree); + const startingPosition = previousNode ? previousNode.endPosition : nodesInRange[0].startPosition; + const startingIndex = previousNode ? previousNode.endIndex : nodesInRange[0].startIndex; + const endingPosition = nodesInRange[nodesInRange.length - 1].endPosition; + const endingIndex = nodesInRange[nodesInRange.length - 1].endIndex; + + const newChange = { newRange: new Range(startingPosition.row + 1, startingPosition.column + 1, endingPosition.row + 1, endingPosition.column + 1), newRangeStartOffset: startingIndex, newRangeEndOffset: endingIndex }; + if ((mergedChanges.length > 0) && (mergedChanges[mergedChanges.length - 1].newRangeEndOffset >= newChange.newRangeStartOffset)) { + // Merge the changes + mergedChanges[mergedChanges.length - 1].newRange = Range.fromPositions(mergedChanges[mergedChanges.length - 1].newRange.getStartPosition(), newChange.newRange.getEndPosition()); + mergedChanges[mergedChanges.length - 1].newRangeEndOffset = newChange.newRangeEndOffset; + } else { + mergedChanges.push(newChange); } } - return ranges; + return mergedChanges; } private _onDidChangeContentQueue: LimitedQueue = new LimitedQueue(); @@ -354,15 +305,19 @@ export class TreeSitterParseResult implements IDisposable, ITreeSitterParseResul return; } - let ranges: RangeChange[] | undefined; + const oldTree = this._lastFullyParsed; + let changedNodes: Parser.Node[] | undefined; if (this._lastFullyParsedWithEdits && this._lastFullyParsed) { - ranges = this.calculateRangeChange(model, this.findChangedNodes(this._lastFullyParsedWithEdits, this._lastFullyParsed)); + changedNodes = this.findChangedNodes(this._lastFullyParsedWithEdits, this._lastFullyParsed); } const completed = await this._parseAndUpdateTree(model, version); if (completed) { - if (!ranges) { - ranges = [{ newRange: model.getFullModelRange(), oldRangeLength: model.getValueLength(), newRangeStartOffset: 0, newRangeEndOffset: model.getValueLength() }]; + let ranges: RangeChange[] | undefined; + if (!changedNodes) { + ranges = [{ newRange: model.getFullModelRange(), newRangeStartOffset: 0, newRangeEndOffset: model.getValueLength() }]; + } else if (oldTree && changedNodes) { + ranges = this.findTreeChanges(completed, changedNodes); } this._onDidUpdate.fire({ ranges, versionId: version }); } diff --git a/src/vs/editor/common/services/treeSitterParserService.ts b/src/vs/editor/common/services/treeSitterParserService.ts index 5e96ebd1617..64e5a870d8f 100644 --- a/src/vs/editor/common/services/treeSitterParserService.ts +++ b/src/vs/editor/common/services/treeSitterParserService.ts @@ -23,7 +23,6 @@ export interface RangeWithOffsets { export interface RangeChange { newRange: Range; - oldRangeLength: number; newRangeStartOffset: number; newRangeEndOffset: number; } diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index 2a82f208c62..f1b89210a4b 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -12,7 +12,8 @@ export namespace AccessibilityHelpNLS { export const editableDiffEditor = nls.localize("editableDiffEditor", "You are in a pane of a diff editor."); export const readonlyEditor = nls.localize("readonlyEditor", "You are in a read-only code editor."); export const editableEditor = nls.localize("editableEditor", "You are in a code editor."); - export const activeEditorState = nls.localize("activeEditorState", "Get information about the active editor such as Modified, Problems, and more by setting activeEditorState as a part of the window.title setting."); + export const defaultWindowTitleIncludesEditorState = nls.localize("defaultWindowTitleIncludesEditorState", "activeEditorState - such as modified, problems, and more, is included as a part of the window.title setting by default. Disable it with accessibility.windowTitleOptimized."); + export const defaultWindowTitleExcludingEditorState = nls.localize("defaultWindowTitleExcludingEditorState", "activeEditorState - such as modified, problems, and more, is currently not included as a part of the window.title setting by default. Enable it with accessibility.windowTitleOptimized."); export const toolbar = nls.localize("toolbar", "Around the workbench, when the screen reader announces you've landed in a toolbar, use narrow keys to navigate between the toolbar's actions."); export const changeConfigToOnMac = nls.localize("changeConfigToOnMac", "Configure the application to be optimized for usage with a Screen Reader (Command+E)."); export const changeConfigToOnWinLinux = nls.localize("changeConfigToOnWinLinux", "Configure the application to be optimized for usage with a Screen Reader (Control+E)."); @@ -41,7 +42,7 @@ export namespace AccessibilityHelpNLS { export const debugExecuteSelection = nls.localize('debugConsole.executeSelection', "The Debug: Execute Selection command{0} will execute the selected text in the debug console.", ''); export const chatEditorModification = nls.localize('chatEditorModification', "The editor contains pending modifications that have been made by chat."); export const chatEditorRequestInProgress = nls.localize('chatEditorRequestInProgress', "The editor is currently waiting for modifications to be made by chat."); - export const chatEditActions = nls.localize('chatEditing.navigation', 'Navigate between edits in the editor with navigate previous{0} and next{1} and accept{2}, reject{3} or view the diff{4} for the current change.', '', '', '', '', ''); + export const chatEditActions = nls.localize('chatEditing.navigation', 'Navigate between edits in the editor with navigate previous{0} and next{1} and accept{2}, reject{3} or view the diff{4} for the current change.', '', '', '', '', ''); } export namespace InspectTokensNLS { diff --git a/src/vs/editor/common/tokens/lineTokens.ts b/src/vs/editor/common/tokens/lineTokens.ts index 72d94d120d4..5546834b94d 100644 --- a/src/vs/editor/common/tokens/lineTokens.ts +++ b/src/vs/editor/common/tokens/lineTokens.ts @@ -9,6 +9,7 @@ import { IPosition } from '../core/position.js'; import { ITextModel } from '../model.js'; import { OffsetRange } from '../core/offsetRange.js'; import { TokenArray, TokenArrayBuilder } from './tokenArray.js'; +import { BugIndicatingError } from '../../../base/common/errors.js'; export interface IViewLineTokens { @@ -101,6 +102,10 @@ export class LineTokens implements IViewLineTokens { ) >>> 0; constructor(tokens: Uint32Array, text: string, decoder: ILanguageIdCodec) { + const tokensLength = tokens.length > 1 ? tokens[tokens.length - 2] : 0; + if (tokensLength !== text.length) { + throw new BugIndicatingError('Token length and text length do not match!'); + } this._tokens = tokens; this._tokensCount = (this._tokens.length >>> 1); this._text = text; diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index 444bac5d610..1aefa04abeb 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -40,6 +40,7 @@ export interface IViewModel extends ICursorSimpleModel { setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void; visibleLinesStabilized(): void; setHasFocus(hasFocus: boolean): void; + setHasWidgetFocus(hasWidgetFocus: boolean): void; onCompositionStart(): void; onCompositionEnd(): void; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 1db59085090..8918d324ce7 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -36,7 +36,7 @@ import { ILineBreaksComputer, ILineBreaksComputerFactory, InjectedText } from '. import { ViewEventHandler } from '../viewEventHandler.js'; import { ICoordinatesConverter, InlineDecoration, IViewModel, IWhitespaceChangeAccessor, MinimapLinesRenderingData, OverviewRulerDecorationsGroup, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from '../viewModel.js'; import { ViewModelDecorations } from './viewModelDecorations.js'; -import { FocusChangedEvent, HiddenAreasChangedEvent, ModelContentChangedEvent, ModelDecorationsChangedEvent, ModelLanguageChangedEvent, ModelLanguageConfigurationChangedEvent, ModelOptionsChangedEvent, ModelTokensChangedEvent, OutgoingViewModelEvent, ReadOnlyEditAttemptEvent, ScrollChangedEvent, ViewModelEventDispatcher, ViewModelEventsCollector, ViewZonesChangedEvent } from '../viewModelEventDispatcher.js'; +import { FocusChangedEvent, HiddenAreasChangedEvent, ModelContentChangedEvent, ModelDecorationsChangedEvent, ModelLanguageChangedEvent, ModelLanguageConfigurationChangedEvent, ModelOptionsChangedEvent, ModelTokensChangedEvent, OutgoingViewModelEvent, ReadOnlyEditAttemptEvent, ScrollChangedEvent, ViewModelEventDispatcher, ViewModelEventsCollector, ViewZonesChangedEvent, WidgetFocusChangedEvent } from '../viewModelEventDispatcher.js'; import { IViewModelLines, ViewModelLinesFromModelAsIs, ViewModelLinesFromProjectedModel } from './viewModelLines.js'; import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { GlyphMarginLanesModel } from './glyphLanesModel.js'; @@ -216,6 +216,10 @@ export class ViewModel extends Disposable implements IViewModel { this._eventDispatcher.emitOutgoingEvent(new FocusChangedEvent(!hasFocus, hasFocus)); } + public setHasWidgetFocus(hasWidgetFocus: boolean): void { + this._eventDispatcher.emitOutgoingEvent(new WidgetFocusChangedEvent(!hasWidgetFocus, hasWidgetFocus)); + } + public onCompositionStart(): void { this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewCompositionStartEvent()); } diff --git a/src/vs/editor/common/viewModelEventDispatcher.ts b/src/vs/editor/common/viewModelEventDispatcher.ts index 7fddb80251f..fc91875b319 100644 --- a/src/vs/editor/common/viewModelEventDispatcher.ts +++ b/src/vs/editor/common/viewModelEventDispatcher.ts @@ -176,6 +176,7 @@ export class ViewModelEventsCollector { export type OutgoingViewModelEvent = ( ContentSizeChangedEvent | FocusChangedEvent + | WidgetFocusChangedEvent | ScrollChangedEvent | ViewZonesChangedEvent | HiddenAreasChangedEvent @@ -192,6 +193,7 @@ export type OutgoingViewModelEvent = ( export const enum OutgoingViewModelEventKind { ContentSizeChanged, FocusChanged, + WidgetFocusChanged, ScrollChanged, ViewZonesChanged, HiddenAreasChanged, @@ -262,6 +264,30 @@ export class FocusChangedEvent { } } +export class WidgetFocusChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.WidgetFocusChanged; + + readonly oldHasFocus: boolean; + readonly hasFocus: boolean; + + constructor(oldHasFocus: boolean, hasFocus: boolean) { + this.oldHasFocus = oldHasFocus; + this.hasFocus = hasFocus; + } + + public isNoOp(): boolean { + return (this.oldHasFocus === this.hasFocus); + } + + public attemptToMerge(other: OutgoingViewModelEvent): OutgoingViewModelEvent | null { + if (other.kind !== this.kind) { + return null; + } + return new FocusChangedEvent(this.oldHasFocus, other.hasFocus); + } +} + export class ScrollChangedEvent { public readonly kind = OutgoingViewModelEventKind.ScrollChanged; diff --git a/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/src/vs/editor/contrib/clipboard/browser/clipboard.ts index a5bc65bc100..aaf90905781 100644 --- a/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -157,7 +157,7 @@ class ExecCommandCopyWithSyntaxHighlightingAction extends EditorAction { constructor() { super({ id: 'editor.action.clipboardCopyWithSyntaxHighlightingAction', - label: nls.localize2('actions.clipboard.copyWithSyntaxHighlightingLabel', "Copy With Syntax Highlighting"), + label: nls.localize2('actions.clipboard.copyWithSyntaxHighlightingLabel', "Copy with Syntax Highlighting"), precondition: undefined, kbOpts: { kbExpr: EditorContextKeys.textInputFocus, diff --git a/src/vs/editor/contrib/codelens/browser/codelensController.ts b/src/vs/editor/contrib/codelens/browser/codelensController.ts index 85cc79d7ca3..bd54868ea99 100644 --- a/src/vs/editor/contrib/codelens/browser/codelensController.ts +++ b/src/vs/editor/contrib/codelens/browser/codelensController.ts @@ -465,7 +465,7 @@ registerEditorAction(class ShowLensesInCurrentLine extends EditorAction { super({ id: 'codelens.showLensesInCurrentLine', precondition: EditorContextKeys.hasCodeLensProvider, - label: localize2('showLensOnLine', "Show CodeLens Commands For Current Line"), + label: localize2('showLensOnLine', "Show CodeLens Commands for Current Line"), }); } diff --git a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts index 3a00c1d8f96..e6043ebbd33 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts @@ -15,7 +15,7 @@ import { DynamicCssRules } from '../../../browser/editorDom.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; import { Position } from '../../../common/core/position.js'; import { Range } from '../../../common/core/range.js'; -import { IEditorContribution } from '../../../common/editorCommon.js'; +import { IEditorContribution, IEditorDecorationsCollection } from '../../../common/editorCommon.js'; import { IModelDecoration, IModelDeltaDecoration } from '../../../common/model.js'; import { ModelDecorationOptions } from '../../../common/model/textModel.js'; import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js'; @@ -40,12 +40,12 @@ export class ColorDetector extends Disposable implements IEditorContribution { private _decorationsIds: string[] = []; private _colorDatas = new Map(); - private readonly _colorDecoratorIds = this._editor.createDecorationsCollection(); + private readonly _colorDecoratorIds: IEditorDecorationsCollection; private _isColorDecoratorsEnabled: boolean; private _defaultColorDecoratorsEnablement: 'auto' | 'always' | 'never'; - private readonly _ruleFactory = new DynamicCssRules(this._editor); + private readonly _ruleFactory: DynamicCssRules; private readonly _decoratorLimitReporter = new DecoratorLimitReporter(); @@ -56,6 +56,8 @@ export class ColorDetector extends Disposable implements IEditorContribution { @ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService, ) { super(); + this._colorDecoratorIds = this._editor.createDecorationsCollection(); + this._ruleFactory = new DynamicCssRules(this._editor); this._debounceInformation = languageFeatureDebounceService.for(_languageFeaturesService.colorProvider, 'Document Colors', { min: ColorDetector.RECOMPUTE_TIME }); this._register(_editor.onDidChangeModel(() => { this._isColorDecoratorsEnabled = this.isEnabled(); diff --git a/src/vs/editor/contrib/find/browser/findController.ts b/src/vs/editor/contrib/find/browser/findController.ts index 6b6102ea5ac..9e00a1d5d47 100644 --- a/src/vs/editor/contrib/find/browser/findController.ts +++ b/src/vs/editor/contrib/find/browser/findController.ts @@ -584,7 +584,7 @@ export class StartFindWithArgsAction extends EditorAction { constructor() { super({ id: FIND_IDS.StartFindWithArgs, - label: nls.localize2('startFindWithArgsAction', "Find With Arguments"), + label: nls.localize2('startFindWithArgsAction', "Find with Arguments"), precondition: undefined, kbOpts: { kbExpr: null, @@ -633,7 +633,7 @@ export class StartFindWithSelectionAction extends EditorAction { constructor() { super({ id: FIND_IDS.StartFindWithSelection, - label: nls.localize2('startFindWithSelectionAction', "Find With Selection"), + label: nls.localize2('startFindWithSelectionAction', "Find with Selection"), precondition: undefined, kbOpts: { kbExpr: null, diff --git a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts index 33c8ce33323..d8828c06915 100644 --- a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts @@ -26,7 +26,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILabelService } from '../../../../platform/label/common/label.js'; import { IMarker, IRelatedInformation, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { contrastBorder, editorBackground, editorErrorBorder, editorErrorForeground, editorInfoBorder, editorInfoForeground, editorWarningBorder, editorWarningForeground, oneOf, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; import { IColorTheme, IThemeChangeEvent, IThemeService } from '../../../../platform/theme/common/themeService.js'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index bc5d723722f..1658944cc6d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -243,7 +243,7 @@ export class InlineCompletionsController extends Disposable { const currentInlineCompletionBySemanticId = derivedObservableWithCache(this, (reader, last) => { const model = this.model.read(reader); - const state = model?.inlineCompletionState.read(reader); + const state = model?.state.read(reader); if (this._suggestWidgetAdapter.selectedItem.get()) { return last; } @@ -256,16 +256,20 @@ export class InlineCompletionsController extends Disposable { }), async (_value, _, _deltas, store) => { /** @description InlineCompletionsController.playAccessibilitySignalAndReadSuggestion */ const model = this.model.get(); - const state = model?.inlineCompletionState.get(); + const state = model?.state.get(); if (!state || !model) { return; } - const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); + const lineText = state.kind === 'ghostText' ? model.textModel.getLineContent(state.primaryGhostText.lineNumber) : ''; await timeout(50, cancelOnDispose(store)); await waitForState(this._suggestWidgetAdapter.selectedItem, isUndefined, () => false, cancelOnDispose(store)); await this._accessibilitySignalService.playSignal(AccessibilitySignal.inlineSuggestion); if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) { - this._provideScreenReaderUpdate(state.primaryGhostText.renderForScreenReader(lineText)); + if (state.kind === 'ghostText') { + this._provideScreenReaderUpdate(state.primaryGhostText.renderForScreenReader(lineText)); + } else { + this._provideScreenReaderUpdate(''); // Only announce Alt+F2 + } } })); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts index 08471590470..d5b54d82af8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts @@ -172,9 +172,9 @@ export class InlineSuggestionHintsContentWidget extends Disposable implements IC return action; } - private readonly previousAction = this.createCommandAction(showPreviousInlineSuggestionActionId, localize('previous', 'Previous'), ThemeIcon.asClassName(inlineSuggestionHintsPreviousIcon)); - private readonly availableSuggestionCountAction = new Action('inlineSuggestionHints.availableSuggestionCount', '', undefined, false); - private readonly nextAction = this.createCommandAction(showNextInlineSuggestionActionId, localize('next', 'Next'), ThemeIcon.asClassName(inlineSuggestionHintsNextIcon)); + private readonly previousAction = this._register(this.createCommandAction(showPreviousInlineSuggestionActionId, localize('previous', 'Previous'), ThemeIcon.asClassName(inlineSuggestionHintsPreviousIcon))); + private readonly availableSuggestionCountAction = this._register(new Action('inlineSuggestionHints.availableSuggestionCount', '', undefined, false)); + private readonly nextAction = this._register(this.createCommandAction(showNextInlineSuggestionActionId, localize('next', 'Next'), ThemeIcon.asClassName(inlineSuggestionHintsNextIcon))); private readonly toolBar: CustomizedMenuWorkbenchToolBar; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts index 0a4e864d2c6..959a99ea366 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts @@ -14,12 +14,16 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { InlineCompletionsModel } from './model/inlineCompletionsModel.js'; +import { TextEdit } from '../../../common/core/textEdit.js'; +import { LineEdit } from '../../../common/core/lineEdit.js'; +import { TextModelText } from '../../../common/model/textModelText.js'; +import { localize } from '../../../../nls.js'; export class InlineCompletionsAccessibleView implements IAccessibleViewImplementation { readonly type = AccessibleViewType.View; readonly priority = 95; readonly name = 'inline-completions'; - readonly when = ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible); + readonly when = ContextKeyExpr.or(InlineCompletionContextKeys.inlineSuggestionVisible, InlineCompletionContextKeys.inlineEditVisible); getProvider(accessor: ServicesAccessor) { const codeEditorService = accessor.get(ICodeEditorService); const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); @@ -28,7 +32,7 @@ export class InlineCompletionsAccessibleView implements IAccessibleViewImplement } const model = InlineCompletionsController.get(editor)?.model.get(); - if (!model?.inlineCompletionState.get()) { + if (!model?.state.get()) { return; } @@ -51,16 +55,23 @@ class InlineCompletionsAccessibleViewContentProvider extends Disposable implemen public readonly options = { language: this._editor.getModel()?.getLanguageId() ?? undefined, type: AccessibleViewType.View }; public provideContent(): string { - const state = this._model.inlineCompletionState.get(); + const state = this._model.state.get(); if (!state) { throw new Error('Inline completion is visible but state is not available'); } - const lineText = this._model.textModel.getLineContent(state.primaryGhostText.lineNumber); - const ghostText = state.primaryGhostText.renderForScreenReader(lineText); - if (!ghostText) { - throw new Error('Inline completion is visible but ghost text is not available'); + if (state.kind === 'ghostText') { + + const lineText = this._model.textModel.getLineContent(state.primaryGhostText.lineNumber); + const ghostText = state.primaryGhostText.renderForScreenReader(lineText); + if (!ghostText) { + throw new Error('Inline completion is visible but ghost text is not available'); + } + return lineText + ghostText; + } else { + const text = new TextModelText(this._model.textModel); + const lineEdit = LineEdit.fromTextEdit(new TextEdit(state.edits), text); + return localize('inlineEditAvailable', 'There is an inline edit available:') + '\n' + lineEdit.humanReadablePatch(text.getLines()); } - return lineText + ghostText; } public provideNextContent(): string | undefined { // asynchronously update the model and fire the event diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts index 2e91840d292..91d80882aaf 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts @@ -8,7 +8,7 @@ import { autorunWithStore } from '../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../browser/editorBrowser.js'; import { CodeEditorWidget } from '../../../../browser/widget/codeEditor/codeEditorWidget.js'; -import { IRecordableEditorLogEntry, StructuredLogger } from './inlineCompletionsSource.js'; +import { IRecordableEditorLogEntry, StructuredLogger } from '../structuredLogger.js'; export class TextModelChangeRecorder extends Disposable { private readonly _structuredLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast(), diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts index 40411f027ef..700bc76bf64 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts @@ -68,7 +68,11 @@ export interface IGhostTextLine { lineDecorations: LineDecoration[]; } + export class GhostTextPart { + + readonly lines: IGhostTextLine[]; + constructor( readonly column: number, readonly text: string, @@ -78,13 +82,12 @@ export class GhostTextPart { readonly preview: boolean, private _inlineDecorations: InlineDecoration[] = [], ) { + this.lines = splitLines(this.text).map((line, i) => ({ + line, + lineDecorations: LineDecoration.filter(this._inlineDecorations, i + 1, 1, line.length + 1) + })); } - readonly lines: IGhostTextLine[] = splitLines(this.text).map((line, i) => ({ - line, - lineDecorations: LineDecoration.filter(this._inlineDecorations, i + 1, 1, line.length + 1) - })); - equals(other: GhostTextPart): boolean { return this.column === other.column && this.lines.length === other.lines.length && @@ -96,22 +99,24 @@ export class GhostTextPart { } export class GhostTextReplacement { - public readonly parts: ReadonlyArray = [ - new GhostTextPart( - this.columnRange.endColumnExclusive, - this.text, - false - ), - ]; + public readonly parts: ReadonlyArray; + readonly newLines: string[]; constructor( readonly lineNumber: number, readonly columnRange: ColumnRange, readonly text: string, public readonly additionalReservedLineCount: number = 0, - ) { } - - readonly newLines = splitLines(this.text); + ) { + this.parts = [ + new GhostTextPart( + this.columnRange.endColumnExclusive, + this.text, + false + ), + ]; + this.newLines = splitLines(this.text); + } renderForScreenReader(_lineText: string): string { return this.newLines.join('\n'); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index efcbda1a33d..63deb8574a4 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -771,7 +771,7 @@ export class InlineCompletionsModel extends Disposable { // TODO: clean this up if we keep it private readonly _inAcceptPartialFlow = observableValue(this, false); - public readonly inAcceptPartialFlow: IObservable = this._inAcceptPartialFlow; + public readonly inPartialAcceptFlow: IObservable = this._inAcceptPartialFlow; public async acceptNextInlineEditPart(editor: ICodeEditor): Promise { if (editor.getModel() !== this.textModel) { throw new BugIndicatingError(); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index d9d9f0e0991..c215dba8dc4 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -10,11 +10,9 @@ import { equalsIfDefined, itemEquals } from '../../../../../base/common/equals.j import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { matchesSubString } from '../../../../../base/common/filters.js'; import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, IObservableWithChange, IReader, ITransaction, derived, derivedHandleChanges, disposableObservableValue, observableFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { IObservable, IObservableWithChange, IReader, ITransaction, derived, derivedHandleChanges, disposableObservableValue, observableValue, transaction } from '../../../../../base/common/observable.js'; import { commonPrefixLength, commonSuffixLength, splitLines } from '../../../../../base/common/strings.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; @@ -34,6 +32,7 @@ import { ILanguageFeaturesService } from '../../../../common/services/languageFe import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from './provideInlineCompletions.js'; import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; +import { StructuredLogger, IRecordableEditorLogEntry, IRecordableLogEntry, formatRecordableLogEntry } from '../structuredLogger.js'; export class InlineCompletionsSource extends Disposable { private static _requestId = 0; @@ -145,7 +144,7 @@ export class InlineCompletionsSource extends Disposable { const result = updatedCompletions?.completions.map(c => ({ range: c.range.toString(), text: c.insertText, - isInlineEdit: !!c.sourceInlineCompletion.isInlineEdit, + isInlineEdit: !!c.isInlineEdit, source: c.source.provider.groupId, })); this._log({ sourceId: 'InlineCompletions.fetch', kind: 'end', requestId, durationMs: (Date.now() - startTime.getTime()), error, result, time: Date.now() }); @@ -318,25 +317,12 @@ export class InlineCompletionWithUpdatedRange extends Disposable { return this.source.inlineCompletions.enableForwardStability ?? false; } - private _updatedEdit: UpdatedEdit; // helper as derivedHandleChanges can not access previous value - - public get updatedEdit(): IObservable { return this._updatedEdit.offsetEdit; } - - private readonly _updatedRange = derived(reader => { - const edit = this.updatedEdit.read(reader); - if (!edit || edit.edits.length === 0) { - return undefined; - } - - return Range.fromPositions( - this._textModel.getPositionAt(edit.edits[0].replaceRange.start), - this._textModel.getPositionAt(edit.edits[edit.edits.length - 1].replaceRange.endExclusive) - ); - }); + private readonly _updatedEditObj: UpdatedEdit; // helper as derivedHandleChanges can not access previous value + public get updatedEdit(): IObservable { return this._updatedEditObj.offsetEdit; } public get source() { return this.inlineCompletion.source; } public get sourceInlineCompletion() { return this.inlineCompletion.sourceInlineCompletion; } - public get isInlineEdit() { return this.inlineCompletion.sourceInlineCompletion.isInlineEdit; } + public get isInlineEdit() { return this.inlineCompletion.isInlineEdit; } constructor( public readonly inlineCompletion: InlineCompletionItem, @@ -347,68 +333,7 @@ export class InlineCompletionWithUpdatedRange extends Disposable { ) { super(); - this._updatedEdit = this._register(this._toUpdatedEdit(updatedRange ?? this.inlineCompletion.range, this.inlineCompletion.insertText)); - } - - private _toInlineCompletionEdit(editRange: Range, replaceText: string): UpdatedEdit { - const startOffset = this._textModel.getOffsetAt(editRange.getStartPosition()); - const endOffset = this._textModel.getOffsetAt(editRange.getEndPosition()); - const originalRange = OffsetRange.ofStartAndLength(startOffset, endOffset - startOffset); - - const offsetEdit = new OffsetEdit([new SingleOffsetEdit(originalRange, replaceText)]); - - return new UpdatedEdit(offsetEdit, this._textModel, this._modelVersion, false); - } - - private _toUpdatedEdit(editRange: Range, replaceText: string): UpdatedEdit { - if (!this.isInlineEdit) { - return this._toInlineCompletionEdit(editRange, replaceText); - } - - const eol = this._textModel.getEOL(); - const editOriginalText = this._textModel.getValueInRange(editRange); - const editReplaceText = replaceText.replace(/\r\n|\r|\n/g, eol); - - const diffAlgorithm = linesDiffComputers.getDefault(); - const lineDiffs = diffAlgorithm.computeDiff( - splitLines(editOriginalText), - splitLines(editReplaceText), - { - ignoreTrimWhitespace: false, - computeMoves: false, - extendToSubwords: true, - maxComputationTimeMs: 500, - } - ); - - const innerChanges = lineDiffs.changes.flatMap(c => c.innerChanges ?? []); - - function addRangeToPos(pos: Position, range: Range): Range { - const start = TextLength.fromPosition(range.getStartPosition()); - return TextLength.ofRange(range).createRange(start.addToPosition(pos)); - } - - const modifiedText = new StringText(editReplaceText); - - const offsetEdit = new OffsetEdit( - innerChanges.map(c => { - const range = addRangeToPos(editRange.getStartPosition(), c.originalRange); - const startOffset = this._textModel.getOffsetAt(range.getStartPosition()); - const endOffset = this._textModel.getOffsetAt(range.getEndPosition()); - const originalRange = OffsetRange.ofStartAndLength(startOffset, endOffset - startOffset); - - // TODO: EOL are not properly trimmed by the diffAlgorithm #12680 - const replaceText = modifiedText.getValueOfRange(c.modifiedRange); - const oldText = this._textModel.getValueInRange(range); - if (replaceText.endsWith(eol) && oldText.endsWith(eol)) { - return new SingleOffsetEdit(originalRange.deltaEnd(-eol.length), replaceText.slice(0, -eol.length)); - } - - return new SingleOffsetEdit(originalRange, replaceText); - }) - ); - - return new UpdatedEdit(offsetEdit, this._textModel, this._modelVersion, true); + this._updatedEditObj = this._register(this._toUpdatedEdit(updatedRange ?? this.inlineCompletion.range, this.inlineCompletion.insertText)); } public toInlineCompletion(reader: IReader | undefined): InlineCompletionItem { @@ -441,7 +366,7 @@ export class InlineCompletionWithUpdatedRange extends Disposable { } public isVisible(model: ITextModel, cursorPosition: Position, reader: IReader | undefined): boolean { - const minimizedReplacement = singleTextRemoveCommonPrefix(this._toFilterTextReplacement(reader), model); + const minimizedReplacement = singleTextRemoveCommonPrefix(this.toSingleTextEdit(reader), model); const updatedRange = this._updatedRange.read(reader); if ( !updatedRange @@ -487,7 +412,7 @@ export class InlineCompletionWithUpdatedRange extends Disposable { } if (this.sourceInlineCompletion.isInlineEdit) { - return this._updatedEdit.lastChangePartOfInlineEdit; + return this._updatedEditObj.lastChangePartOfInlineEdit; } const updatedRange = this._updatedRange.read(undefined); @@ -498,9 +423,74 @@ export class InlineCompletionWithUpdatedRange extends Disposable { return result; } - private _toFilterTextReplacement(reader: IReader | undefined): SingleTextEdit { - const inlineCompletion = this.toInlineCompletion(reader); - return new SingleTextEdit(inlineCompletion.range, inlineCompletion.filterText); + private readonly _updatedRange = derived(reader => { + const edit = this.updatedEdit.read(reader); + if (!edit || edit.edits.length === 0) { + return undefined; + } + + return Range.fromPositions( + this._textModel.getPositionAt(edit.edits[0].replaceRange.start), + this._textModel.getPositionAt(edit.edits[edit.edits.length - 1].replaceRange.endExclusive) + ); + }); + + private _toUpdatedEdit(editRange: Range, replaceText: string): UpdatedEdit { + return this.isInlineEdit + ? this._toInlineEditEdit(editRange, replaceText) + : this._toInlineCompletionEdit(editRange, replaceText); + } + + private _toInlineCompletionEdit(editRange: Range, replaceText: string): UpdatedEdit { + const startOffset = this._textModel.getOffsetAt(editRange.getStartPosition()); + const endOffset = this._textModel.getOffsetAt(editRange.getEndPosition()); + const originalRange = OffsetRange.ofStartAndLength(startOffset, endOffset - startOffset); + const offsetEdit = new OffsetEdit([new SingleOffsetEdit(originalRange, replaceText)]); + return new UpdatedEdit(offsetEdit, this._textModel, this._modelVersion, false); + } + + private _toInlineEditEdit(editRange: Range, replaceText: string): UpdatedEdit { + const eol = this._textModel.getEOL(); + const editOriginalText = this._textModel.getValueInRange(editRange); + const editReplaceText = replaceText.replace(/\r\n|\r|\n/g, eol); + + const diffAlgorithm = linesDiffComputers.getDefault(); + const lineDiffs = diffAlgorithm.computeDiff( + splitLines(editOriginalText), + splitLines(editReplaceText), + { + ignoreTrimWhitespace: false, + computeMoves: false, + extendToSubwords: true, + maxComputationTimeMs: 500, + } + ); + + const innerChanges = lineDiffs.changes.flatMap(c => c.innerChanges ?? []); + + function addRangeToPos(pos: Position, range: Range): Range { + const start = TextLength.fromPosition(range.getStartPosition()); + return TextLength.ofRange(range).createRange(start.addToPosition(pos)); + } + + const modifiedText = new StringText(editReplaceText); + + const offsetEdit = new OffsetEdit( + innerChanges.map(c => { + const range = addRangeToPos(editRange.getStartPosition(), c.originalRange); + const startOffset = this._textModel.getOffsetAt(range.getStartPosition()); + const endOffset = this._textModel.getOffsetAt(range.getEndPosition()); + const originalRange = OffsetRange.ofStartAndLength(startOffset, endOffset - startOffset); + + const replaceText = modifiedText.getValueOfRange(c.modifiedRange); + const originalText = this._textModel.getValueInRange(range); + const edit = new SingleOffsetEdit(originalRange, replaceText); + + return reshapeEdit(edit, originalText, innerChanges.length, this._textModel); + }) + ); + + return new UpdatedEdit(offsetEdit, this._textModel, this._modelVersion, true); } } @@ -737,50 +727,59 @@ class SingleUpdatedNextEdit extends SingleUpdatedEdit { const emptyRange = new Range(1, 1, 1, 1); -interface IRecordableLogEntry { - sourceId: string; - time: number; -} - -export interface IRecordableEditorLogEntry extends IRecordableLogEntry { - modelUri: string; - modelVersion: number; -} - -/** - * The sourceLabel must not contain '@'! -*/ -export function formatRecordableLogEntry(entry: T): string { - return entry.sourceId + ' @@ ' + JSON.stringify({ ...entry, sourceId: undefined }); -} - -export class StructuredLogger extends Disposable { - public static cast(): typeof StructuredLogger { - return this as typeof StructuredLogger; +function reshapeEdit(edit: SingleOffsetEdit, originalText: string, totalInnerEdits: number, textModel: ITextModel): SingleOffsetEdit { + // TODO: EOL are not properly trimmed by the diffAlgorithm #12680 + const eol = textModel.getEOL(); + if (edit.newText.endsWith(eol) && originalText.endsWith(eol)) { + edit = new SingleOffsetEdit(edit.replaceRange.deltaEnd(-eol.length), edit.newText.slice(0, -eol.length)); } - private readonly _contextKeyValue = observableContextKey(this._contextKey, this._contextKeyService).recomputeInitiallyAndOnChange(this._store); - - constructor( - private readonly _contextKey: string, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @ICommandService private readonly _commandService: ICommandService, - ) { - super(); + // INSERTION + // If the insertion ends with a new line and is inserted at the start of a line which has text, + // we move the insertion to the end of the previous line if possible + if (totalInnerEdits === 1 && edit.replaceRange.isEmpty && edit.newText.includes(eol)) { + edit = reshapeMultiLineInsertion(edit, textModel); } - public readonly isEnabled = this._contextKeyValue.map(v => v !== undefined); + // The diff algorithm extended a simple edit to the entire word + // shrink it back to a simple edit if it is deletion/insertion only + if (totalInnerEdits === 1) { + const prefixLength = commonPrefixLength(originalText, edit.newText); + const suffixLength = commonSuffixLength(originalText.slice(prefixLength), edit.newText.slice(prefixLength)); - public log(data: T): boolean { - const commandId = this._contextKeyValue.get(); - if (!commandId) { - return false; + // reshape it back to an insertion + if (prefixLength + suffixLength === originalText.length) { + return new SingleOffsetEdit(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), edit.newText.substring(prefixLength, edit.newText.length - suffixLength)); + } + + // reshape it back to a deletion + if (prefixLength + suffixLength === edit.newText.length) { + return new SingleOffsetEdit(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), ''); } - this._commandService.executeCommand(commandId, data); - return true; } + + return edit; } -export function observableContextKey(key: string, contextKeyService: IContextKeyService): IObservable { - return observableFromEvent(contextKeyService.onDidChangeContext, () => contextKeyService.getContextKeyValue(key)); +function reshapeMultiLineInsertion(edit: SingleOffsetEdit, textModel: ITextModel): SingleOffsetEdit { + if (!edit.replaceRange.isEmpty) { + throw new BugIndicatingError('Unexpected original range'); + } + + if (edit.replaceRange.start === 0) { + return edit; + } + + const eol = textModel.getEOL(); + const startPosition = textModel.getPositionAt(edit.replaceRange.start); + const startColumn = startPosition.column; + const startLineNumber = startPosition.lineNumber; + + // If the insertion ends with a new line and is inserted at the start of a line which has text, + // we move the insertion to the end of the previous line if possible + if (startColumn === 1 && startLineNumber > 1 && textModel.getLineLength(startLineNumber) !== 0 && edit.newText.endsWith(eol) && !edit.newText.startsWith(eol)) { + return new SingleOffsetEdit(edit.replaceRange.delta(-1), eol + edit.newText.slice(0, -eol.length)); + } + + return edit; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts index 63e45b69ac7..8ccfc9609b0 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts @@ -10,7 +10,7 @@ import { InlineCompletionItem } from './provideInlineCompletions.js'; export class InlineEdit { constructor( public readonly edit: SingleTextEdit, - public readonly renderExplicitly: boolean, + public readonly jumpedTo: boolean, public readonly commands: readonly Command[], public readonly inlineCompletion: InlineCompletionItem, ) { } @@ -25,7 +25,7 @@ export class InlineEdit { public equals(other: InlineEdit): boolean { return this.edit.equals(other.edit) - && this.renderExplicitly === other.renderExplicitly + && this.jumpedTo === other.jumpedTo && this.inlineCompletion === other.inlineCompletion; } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index f510bfab853..47cdd05852a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -180,10 +180,10 @@ async function addRefAndCreateResult( completions.addRef(); lists.push(completions); for (const item of completions.inlineCompletions.items) { - if (!context.includeInlineEdits && item.isInlineEdit) { + if (!context.includeInlineEdits && (item.isInlineEdit || item.showInlineEditMenu)) { continue; } - if (!context.includeInlineCompletions && !item.isInlineEdit) { + if (!context.includeInlineCompletions && !(item.isInlineEdit || item.showInlineEditMenu)) { continue; } const inlineCompletionItem = InlineCompletionItem.from( @@ -197,7 +197,7 @@ async function addRefAndCreateResult( itemsByHash.set(inlineCompletionItem.hash(), inlineCompletionItem); // Stop after first visible inline completion - if (!item.isInlineEdit && context.triggerKind === InlineCompletionTriggerKind.Automatic) { + if (!(item.isInlineEdit || item.showInlineEditMenu) && context.triggerKind === InlineCompletionTriggerKind.Automatic) { const minifiedEdit = inlineCompletionItem.toSingleTextEdit().removeCommonPrefix(new TextModelText(model)); if (!minifiedEdit.isEmpty) { shouldStop = true; @@ -370,9 +370,10 @@ export class InlineCompletionItem { readonly id = `InlineCompletion:${InlineCompletionItem.ID++}`, ) { - // TODO: these statements are no-ops - filterText = filterText.replace(/\r\n|\r/g, '\n'); - insertText = filterText.replace(/\r\n|\r/g, '\n'); + } + + get isInlineEdit(): boolean { + return this.sourceInlineCompletion.isInlineEdit!!; } public get didShow(): boolean { @@ -382,23 +383,6 @@ export class InlineCompletionItem { this._didCallShow = true; } - public withRange(updatedRange: Range): InlineCompletionItem { - return new InlineCompletionItem( - this.filterText, - this.command, - this.shownCommand, - this.action, - updatedRange, - this.insertText, - this.snippetInfo, - this.cursorShowRange, - this.additionalTextEdits, - this.sourceInlineCompletion, - this.source, - this.id, - ); - } - public withRangeInsertTextAndFilterText(updatedRange: Range, updatedInsertText: string, updatedFilterText: string): InlineCompletionItem { return new InlineCompletionItem( updatedFilterText, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/structuredLogger.ts b/src/vs/editor/contrib/inlineCompletions/browser/structuredLogger.ts new file mode 100644 index 00000000000..8a307df3bd3 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/structuredLogger.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IObservable, observableFromEvent } from '../../../../base/common/observable.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; + +export interface IRecordableLogEntry { + sourceId: string; + time: number; +} + +export interface IRecordableEditorLogEntry extends IRecordableLogEntry { + modelUri: string; + modelVersion: number; +} + +/** + * The sourceLabel must not contain '@'! +*/ +export function formatRecordableLogEntry(entry: T): string { + return entry.sourceId + ' @@ ' + JSON.stringify({ ...entry, sourceId: undefined }); +} + +export class StructuredLogger extends Disposable { + public static cast(): typeof StructuredLogger { + return this as typeof StructuredLogger; + } + + public readonly isEnabled; + private readonly _contextKeyValue; + + constructor( + private readonly _contextKey: string, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ICommandService private readonly _commandService: ICommandService + ) { + super(); + this._contextKeyValue = observableContextKey(this._contextKey, this._contextKeyService).recomputeInitiallyAndOnChange(this._store); + this.isEnabled = this._contextKeyValue.map(v => v !== undefined); + } + + public log(data: T): boolean { + const commandId = this._contextKeyValue.get(); + if (!commandId) { + return false; + } + this._commandService.executeCommand(commandId, data); + return true; + } +} + +function observableContextKey(key: string, contextKeyService: IContextKeyService): IObservable { + return observableFromEvent(contextKeyService.onDidChangeContext, () => contextKeyService.getContextKeyValue(key)); +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css index 3bcb8b5f61a..16148bd87b0 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css @@ -33,6 +33,8 @@ z-index: 1; } +.monaco-editor .ghost-text-decoration.clickable, +.monaco-editor .ghost-text-decoration-preview.clickable, .monaco-editor .suggest-preview-text.clickable .ghost-text { cursor: pointer; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index fb04686f119..d592eca09ea 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -31,6 +31,7 @@ import { ColumnRange } from '../../utils.js'; import { addDisposableListener, getWindow, isHTMLElement, n } from '../../../../../../base/browser/dom.js'; import './ghostTextView.css'; import { IMouseEvent, StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; +import { CodeEditorWidget } from '../../../../../browser/widget/codeEditor/codeEditorWidget.js'; export interface IGhostTextWidgetModel { readonly targetTextModel: IObservable; @@ -81,7 +82,7 @@ export class GhostTextView extends Disposable { return; } const a = e.target.detail.injectedText?.options.attachedData; - if (a instanceof GhostTextAttachedData) { + if (a instanceof GhostTextAttachedData && a.owner === this) { this._onDidClick.fire(e.event); } })); @@ -224,9 +225,12 @@ export class GhostTextView extends Disposable { after: { content: p.text, tokens: p.tokens, - inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration' + extraClassNames + p.lineDecorations.map(d => ' ' + d.className).join(' '), // TODO: take the ranges into account for line decorations + inlineClassName: (p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration') + + (this._isClickable ? ' clickable' : '') + + extraClassNames + + p.lineDecorations.map(d => ' ' + d.className).join(' '), // TODO: take the ranges into account for line decorations cursorStops: InjectedTextCursorStops.Left, - attachedData: new GhostTextAttachedData(), + attachedData: new GhostTextAttachedData(this), }, showIfCollapsed: true, } @@ -255,7 +259,9 @@ export class GhostTextView extends Disposable { ); private readonly _isInlineTextHovered = this._editorObs.isTargetHovered( - p => p.target.type === MouseTargetType.CONTENT_TEXT && p.target.detail.injectedText?.options.attachedData instanceof GhostTextAttachedData, + p => p.target.type === MouseTargetType.CONTENT_TEXT && + p.target.detail.injectedText?.options.attachedData instanceof GhostTextAttachedData && + p.target.detail.injectedText.options.attachedData.owner === this, this._store ); @@ -274,7 +280,9 @@ export class GhostTextView extends Disposable { } } -class GhostTextAttachedData { } +class GhostTextAttachedData { + constructor(public readonly owner: GhostTextView) { } +} interface WidgetDomElement { ghostTextViewWarningWidgetData?: { @@ -377,6 +385,8 @@ export class AdditionalLinesWidget extends Disposable { this._store ); + private hasBeenAccepted = false; + constructor( private readonly _editor: ICodeEditor, private readonly _lines: IObservable<{ @@ -390,12 +400,17 @@ export class AdditionalLinesWidget extends Disposable { ) { super(); + if (this._editor instanceof CodeEditorWidget && this._shouldKeepCursorStable) { + this._register(this._editor.onBeforeExecuteEdit(e => this.hasBeenAccepted = e.source === 'inlineSuggestion.accept')); + } + this._register(autorun(reader => { /** @description update view zone */ const lines = this._lines.read(reader); this.editorOptionsChanged.read(reader); if (lines) { + this.hasBeenAccepted = false; this.updateLines(lines.lineNumber, lines.additionalLines, lines.minReservedLineCount); } else { this.clear(); @@ -435,7 +450,10 @@ export class AdditionalLinesWidget extends Disposable { renderLines(domNode, tabSize, additionalLines, this._editor.getOptions(), this._isClickable); if (this._isClickable) { - store.add(addDisposableListener(domNode, 'mouseup', (e) => { + store.add(addDisposableListener(domNode, 'mousedown', (e) => { + e.preventDefault(); // This prevents that the editor loses focus + })); + store.add(addDisposableListener(domNode, 'click', (e) => { if (isTargetGhostText(e.target)) { this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)); } @@ -469,7 +487,9 @@ export class AdditionalLinesWidget extends Disposable { if (this._viewZoneInfo) { changeAccessor.removeZone(this._viewZoneInfo.viewZoneId); - this.keepCursorStable(this._viewZoneInfo.lineNumber, -this._viewZoneInfo.heightInLines); + if (!this.hasBeenAccepted) { + this.keepCursorStable(this._viewZoneInfo.lineNumber, -this._viewZoneInfo.heightInLines); + } this._viewZoneInfo = undefined; this._viewZoneHeight.set(undefined, undefined); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts index 04afc6a88f3..bcf120f4996 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts @@ -14,7 +14,7 @@ import { InlineCompletionsHintsWidget } from '../hintsWidget/inlineCompletionsHi import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; import { convertItemsToStableObservables } from '../utils.js'; import { GhostTextView } from './ghostText/ghostTextView.js'; -import { InlineEditsViewAndDiffProducer } from './inlineEdits/viewAndDiffProducer.js'; +import { InlineEditsViewAndDiffProducer } from './inlineEdits/inlineEditsViewProducer.js'; export class InlineCompletionsView extends Disposable { private readonly _ghostTexts = derived(this, (reader) => { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts index 6624abea5fa..848af4d2613 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts @@ -22,21 +22,22 @@ import { asCssVariable, descriptionForeground, editorActionListForeground, edito import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; import { hideInlineCompletionId, inlineSuggestCommitId, jumpToNextInlineEditId, toggleShowCollapsedId } from '../../../controller/commandIds.js'; -import { IInlineEditsViewHost } from '../inlineEditsViewInterface.js'; -import { FirstFnArg, InlineEditTabAction } from '../utils/utils.js'; +import { IInlineEditModel, InlineEditTabAction } from '../inlineEditsViewInterface.js'; +import { FirstFnArg, } from '../utils/utils.js'; export class GutterIndicatorMenuContent { - private readonly _inlineEditsShowCollapsed = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.showCollapsed); + private readonly _inlineEditsShowCollapsed: IObservable; constructor( - private readonly _host: IInlineEditsViewHost, + private readonly _model: IInlineEditModel, private readonly _close: (focusEditor: boolean) => void, private readonly _editorObs: ObservableCodeEditor, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @ICommandService private readonly _commandService: ICommandService, ) { + this._inlineEditsShowCollapsed = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.showCollapsed); } public toDisposableLiveElement(): LiveElement { @@ -60,39 +61,74 @@ export class GutterIndicatorMenuContent { }; }; - // TODO make this menu contributable! - return hoverContent([ - header(this._host.displayName), + const title = header(this._model.displayName); + + const gotoAndAccept = option(createOptionArgs({ + id: 'gotoAndAccept', + title: `${localize('goto', "Go To")} / ${localize('accept', "Accept")}`, + icon: this._model.tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.check : Codicon.arrowRight), + commandId: this._model.tabAction.map(action => action === InlineEditTabAction.Accept ? inlineSuggestCommitId : jumpToNextInlineEditId) + })); + + const reject = option(createOptionArgs({ + id: 'reject', + title: localize('reject', "Reject"), + icon: Codicon.close, + commandId: hideInlineCompletionId + })); + + const extensionCommands = this._model.extensionCommands.map((c, idx) => option(createOptionArgs({ id: c.id + '_' + idx, title: c.title, icon: Codicon.symbolEvent, commandId: c.id, commandArgs: c.arguments }))); + + const toggleCollapsedMode = this._inlineEditsShowCollapsed.map(showCollapsed => showCollapsed ? option(createOptionArgs({ - id: 'gotoAndAccept', title: `${localize('goto', "Go To")} / ${localize('accept', "Accept")}`, - icon: this._host.tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.check : Codicon.arrowRight), - commandId: this._host.tabAction.map(action => action === InlineEditTabAction.Accept ? inlineSuggestCommitId : jumpToNextInlineEditId) + id: 'showExpanded', + title: localize('showExpanded', "Show Expanded"), + icon: Codicon.expandAll, + commandId: toggleShowCollapsedId + })) + : option(createOptionArgs({ + id: 'showCollapsed', + title: localize('showCollapsed', "Show Collapsed"), + icon: Codicon.collapseAll, + commandId: toggleShowCollapsedId + })) + ); + + const settings = option(createOptionArgs({ + id: 'settings', + title: localize('settings', "Settings"), + icon: Codicon.gear, + commandId: 'workbench.action.openSettings', + commandArgs: ['@tag:nextEditSuggestions'] + })); + + const actions = this._model.action ? [this._model.action] : []; + const actionBarFooter = actions.length > 0 ? actionBar( + actions.map(action => ({ + id: action.id, + label: action.title, + enabled: true, + run: () => this._commandService.executeCommand(action.id, ...(action.arguments ?? [])), + class: undefined, + tooltip: action.tooltip ?? action.title })), - option(createOptionArgs({ id: 'reject', title: localize('reject', "Reject"), icon: Codicon.close, commandId: hideInlineCompletionId })), + { hoverDelegate: nativeHoverDelegate /* unable to show hover inside another hover */ } + ) : undefined; + + return hoverContent([ + title, + gotoAndAccept, + reject, separator(), - this._host.extensionCommands?.map(c => c && c.length > 0 ? [ - ...c.map((c, idx) => option(createOptionArgs({ id: c.id + '_' + idx, title: c.title, icon: Codicon.symbolEvent, commandId: c.id, commandArgs: c.arguments }))), - separator() - ] : []), - this._inlineEditsShowCollapsed.map(showCollapsed => showCollapsed ? - option(createOptionArgs({ id: 'showExpanded', title: localize('showExpanded', "Show Expanded"), icon: Codicon.expandAll, commandId: toggleShowCollapsedId })) : - option(createOptionArgs({ id: 'showCollapsed', title: localize('showCollapsed', "Show Collapsed"), icon: Codicon.collapseAll, commandId: toggleShowCollapsedId })) - ), - option(createOptionArgs({ id: 'settings', title: localize('settings', "Settings"), icon: Codicon.gear, commandId: 'workbench.action.openSettings', commandArgs: ['@tag:nextEditSuggestions'] })), - this._host.action.map(action => action ? [ - separator(), - actionBar( - [{ - id: action.id, - label: action.title, - enabled: true, - run: () => this._commandService.executeCommand(action.id, ...(action.arguments ?? [])), - class: undefined, - tooltip: action.tooltip ?? action.title - }], - { hoverDelegate: nativeHoverDelegate /* unable to show hover inside another hover */ } - ) - ] : []) + + ...extensionCommands, + extensionCommands.length ? separator() : undefined, + + toggleCollapsedMode, + settings, + + actionBarFooter ? separator() : undefined, + actionBarFooter ]); } @@ -188,6 +224,7 @@ function actionBar(actions: IAction[], options: IActionBarOptions) { function separator() { return n.div({ + id: 'inline-edit-gutter-indicator-menu-separator', class: 'menu-separator', style: { color: asCssVariable(editorActionListForeground), diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts index e737ff7aa25..77e3b3fc1b4 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts @@ -6,6 +6,7 @@ import { n, trackFocus } from '../../../../../../../base/browser/dom.js'; import { renderIcon } from '../../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../../base/common/codicons.js'; +import { BugIndicatingError } from '../../../../../../../base/common/errors.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../../../base/common/lifecycle.js'; import { IObservable, ISettableObservable, constObservable, derived, observableFromEvent, observableValue, runOnChange } from '../../../../../../../base/common/observable.js'; import { debouncedObservable } from '../../../../../../../base/common/observableInternal/utils.js'; @@ -21,17 +22,28 @@ import { EditorOption } from '../../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../../common/core/lineRange.js'; import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; import { StickyScrollController } from '../../../../../stickyScroll/browser/stickyScrollController.js'; -import { IInlineEditsViewHost } from '../inlineEditsViewInterface.js'; +import { IInlineEditModel, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { inlineEditIndicatorBackground, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorPrimaryForeground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorSecondaryForeground, inlineEditIndicatorsuccessfulBackground, inlineEditIndicatorsuccessfulForeground } from '../theme.js'; -import { InlineEditTabAction, mapOutFalsy, rectToProps } from '../utils/utils.js'; +import { mapOutFalsy, rectToProps } from '../utils/utils.js'; import { GutterIndicatorMenuContent } from './gutterIndicatorMenu.js'; export class InlineEditsGutterIndicator extends Disposable { + + private get model() { + const model = this._model.get(); + if (!model) { throw new BugIndicatingError('Inline Edit Model not available'); } + return model; + } + + private readonly _gutterIndicatorBackgroundColor: IObservable; + private readonly _gutterIndicatorForegroundColor: IObservable; + private readonly _isHoveredOverInlineEditDebounced: IObservable; + constructor( private readonly _editorObs: ObservableCodeEditor, private readonly _originalRange: IObservable, private readonly _verticalOffset: IObservable, - private readonly _host: IInlineEditsViewHost, + private readonly _model: IObservable, private readonly _isHoveringOverInlineEdit: IObservable, private readonly _focusIsInMenu: ISettableObservable, @IHoverService private readonly _hoverService: HoverService, @@ -40,6 +52,21 @@ export class InlineEditsGutterIndicator extends Disposable { ) { super(); + this._gutterIndicatorBackgroundColor = this._tabAction.map(v => { + switch (v) { + case InlineEditTabAction.Inactive: return asCssVariable(inlineEditIndicatorSecondaryBackground); + case InlineEditTabAction.Jump: return asCssVariable(inlineEditIndicatorPrimaryBackground); + case InlineEditTabAction.Accept: return asCssVariable(inlineEditIndicatorsuccessfulBackground); + } + }); + this._gutterIndicatorForegroundColor = this._tabAction.map(v => { + switch (v) { + case InlineEditTabAction.Inactive: return asCssVariable(inlineEditIndicatorSecondaryForeground); + case InlineEditTabAction.Jump: return asCssVariable(inlineEditIndicatorPrimaryForeground); + case InlineEditTabAction.Accept: return asCssVariable(inlineEditIndicatorsuccessfulForeground); + } + }); + this._register(this._editorObs.createOverlayWidget({ domNode: this._indicator.element, position: constObservable(null), @@ -47,20 +74,37 @@ export class InlineEditsGutterIndicator extends Disposable { minContentWidthInPx: constObservable(0), })); + this._isHoveredOverInlineEditDebounced = debouncedObservable(this._isHoveringOverInlineEdit, 100); + if (!accessibilityService.isMotionReduced()) { - const debouncedIsHovering = debouncedObservable(this._isHoveringOverInlineEdit, 100); - this._register(runOnChange(debouncedIsHovering, (isHovering) => { + this._register(runOnChange(this._isHoveredOverInlineEditDebounced, (isHovering) => { if (!isHovering) { return; } - this._iconRef.element.animate([ + + // WIGGLE ANIMATION: + /* this._iconRef.element.animate([ { transform: 'rotate(0) scale(1)', offset: 0 }, { transform: 'rotate(14.4deg) scale(1.1)', offset: 0.15 }, { transform: 'rotate(-14.4deg) scale(1.2)', offset: 0.3 }, { transform: 'rotate(14.4deg) scale(1.1)', offset: 0.45 }, { transform: 'rotate(-14.4deg) scale(1.2)', offset: 0.6 }, { transform: 'rotate(0) scale(1)', offset: 1 } - ], { duration: 800 }); + ], { duration: 800 }); */ + + // PULSE ANIMATION: + this._iconRef.element.animate([ + { + outline: `2px solid ${this._gutterIndicatorBackgroundColor.get()}`, + outlineOffset: '-1px', + offset: 0 + }, + { + outline: `2px solid transparent`, + outlineOffset: '10px', + offset: 1 + }, + ], { duration: 500 }); })); } } @@ -109,12 +153,32 @@ export class InlineEditsGutterIndicator extends Disposable { ? pillRectMoved : pillRectMoved.moveToBeContainedIn(fullViewPort.intersect(targetRect.union(fullViewPort.withHeight(lineHeight)))!); //viewPortWithStickyScroll.intersect(rect)!; + + const docked = rect.containsRect(iconRect) && viewPortWithStickyScroll.containsRect(iconRect); + let iconDirecion = (targetRect.containsRect(iconRect) ? 'right' as const + : iconRect.top > targetRect.top ? 'top' as const : 'bottom' as const); + + let icon; + if (docked && (this._isHoveredOverIconDebounced.read(reader) || this._isHoveredOverInlineEditDebounced.read(reader))) { + icon = renderIcon(Codicon.check); + iconDirecion = 'right'; + } else { + icon = this._tabAction.read(reader) === InlineEditTabAction.Accept ? renderIcon(Codicon.keyboardTab) : renderIcon(Codicon.arrowRight); + } + + let rotation = 0; + switch (iconDirecion) { + case 'right': rotation = 0; break; + case 'bottom': rotation = 90; break; + case 'top': rotation = -90; break; + } + return { rect, + icon, + rotation, + docked, iconRect, - arrowDirection: (targetRect.containsRect(iconRect) ? 'right' as const - : iconRect.top > targetRect.top ? 'top' as const : 'bottom' as const), - docked: rect.containsRect(iconRect) && viewPortWithStickyScroll.containsRect(iconRect), }; }); @@ -122,6 +186,7 @@ export class InlineEditsGutterIndicator extends Disposable { private readonly _hoverVisible = observableValue(this, false); public readonly isHoverVisible: IObservable = this._hoverVisible; private readonly _isHoveredOverIcon = observableValue(this, false); + private readonly _isHoveredOverIconDebounced: IObservable = debouncedObservable(this._isHoveredOverIcon, 100); private _showHover(): void { if (this._hoverVisible.get()) { @@ -131,7 +196,7 @@ export class InlineEditsGutterIndicator extends Disposable { const disposableStore = new DisposableStore(); const content = disposableStore.add(this._instantiationService.createInstance( GutterIndicatorMenuContent, - this._host, + this.model, (focusEditor) => { if (focusEditor) { this._editorObs.editor.focus(); @@ -146,7 +211,7 @@ export class InlineEditsGutterIndicator extends Disposable { disposableStore.add(focusTracker.onDidFocus(() => this._focusIsInMenu.set(true, undefined))); disposableStore.add(toDisposable(() => this._focusIsInMenu.set(false, undefined))); - const h = this._hoverService.showHover({ + const h = this._hoverService.showInstantHover({ target: this._iconRef.element, content: content.element, }) as HoverWidget | undefined; @@ -161,15 +226,21 @@ export class InlineEditsGutterIndicator extends Disposable { } } + private readonly _tabAction = derived(this, reader => { + const model = this._model.read(reader); + if (!model) { return InlineEditTabAction.Inactive; } + return model.tabAction.read(reader); + }); + private readonly _indicator = n.div({ class: 'inline-edits-view-gutter-indicator', onclick: () => { const docked = this._layout.map(l => l && l.docked).get(); this._editorObs.editor.focus(); if (docked) { - this._host.accept(); + this.model.accept(); } else { - this._host.jump(); + this.model.jump(); } }, tabIndex: 0, @@ -199,20 +270,8 @@ export class InlineEditsGutterIndicator extends Disposable { cursor: 'pointer', zIndex: '1000', position: 'absolute', - backgroundColor: this._host.tabAction.map(v => { - switch (v) { - case InlineEditTabAction.Inactive: return asCssVariable(inlineEditIndicatorSecondaryBackground); - case InlineEditTabAction.Jump: return asCssVariable(inlineEditIndicatorPrimaryBackground); - case InlineEditTabAction.Accept: return asCssVariable(inlineEditIndicatorsuccessfulBackground); - } - }), - ['--vscodeIconForeground' as any]: this._host.tabAction.map(v => { - switch (v) { - case InlineEditTabAction.Inactive: return asCssVariable(inlineEditIndicatorSecondaryForeground); - case InlineEditTabAction.Jump: return asCssVariable(inlineEditIndicatorPrimaryForeground); - case InlineEditTabAction.Accept: return asCssVariable(inlineEditIndicatorsuccessfulForeground); - } - }), + backgroundColor: this._gutterIndicatorBackgroundColor, + ['--vscodeIconForeground' as any]: this._gutterIndicatorForegroundColor, borderRadius: '4px', display: 'flex', justifyContent: 'center', @@ -222,20 +281,14 @@ export class InlineEditsGutterIndicator extends Disposable { }, [ n.div({ style: { - rotate: layout.map(l => { - switch (l.arrowDirection) { - case 'right': return '0deg'; - case 'bottom': return '90deg'; - case 'top': return '-90deg'; - } - }), + rotate: layout.map(i => `${i.rotation}deg`), transition: 'rotate 0.2s ease-in-out', display: 'flex', alignItems: 'center', justifyContent: 'center', } }, [ - this._host.tabAction.map(v => v === InlineEditTabAction.Accept ? renderIcon(Codicon.keyboardTab) : renderIcon(Codicon.arrowRight)) + layout.map(i => i.icon), ]) ]), ])).keepUpdated(this._store); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts new file mode 100644 index 00000000000..2c8e66d7227 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { derived, IObservable } from '../../../../../../base/common/observable.js'; +import { localize } from '../../../../../../nls.js'; +import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; +import { StringText, TextEdit } from '../../../../../common/core/textEdit.js'; +import { Command } from '../../../../../common/languages.js'; +import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; +import { InlineCompletionWithUpdatedRange } from '../../model/inlineCompletionsSource.js'; +import { IInlineEditModel, InlineEditTabAction } from './inlineEditsViewInterface.js'; +import { InlineEditWithChanges } from './inlineEditWithChanges.js'; + +export class InlineEditModel implements IInlineEditModel { + + readonly action: Command | undefined; + readonly displayName: string; + readonly extensionCommands: Command[]; + + readonly inAcceptFlow: IObservable; + readonly inPartialAcceptFlow: IObservable; + + constructor( + private readonly _model: InlineCompletionsModel, + readonly inlineEdit: InlineEditWithChanges, + readonly tabAction: IObservable, + ) { + this.action = this.inlineEdit.inlineCompletion.action; + this.displayName = this.inlineEdit.inlineCompletion.source.provider.displayName ?? localize('inlineEdit', "Inline Edit"); + this.extensionCommands = this.inlineEdit.inlineCompletion.source.inlineCompletions.commands ?? []; + + this.inAcceptFlow = this._model.inAcceptFlow; + this.inPartialAcceptFlow = this._model.inPartialAcceptFlow; + } + + accept() { + this._model.accept(); + } + + jump() { + this._model.jump(); + } + + abort(reason: string) { + console.error(reason); // TODO: add logs/telemetry + this._model.stop(); + } + + handleInlineEditShown() { + this._model.handleInlineEditShown(this.inlineEdit.inlineCompletion); + } +} + + +export class GhostTextIndicator { + + readonly model: InlineEditModel; + + constructor( + editor: ICodeEditor, + model: InlineCompletionsModel, + readonly lineRange: LineRange, + inlineCompletion: InlineCompletionWithUpdatedRange, + renderExplicitly: boolean, + ) { + const editorObs = observableCodeEditor(editor); + const tabAction = derived(this, reader => { + if (editorObs.isFocused.read(reader)) { + if (model.inlineCompletionState.read(reader)?.inlineCompletion?.sourceInlineCompletion.showInlineEditMenu) { + return InlineEditTabAction.Accept; + } + } + return InlineEditTabAction.Inactive; + }); + + this.model = new InlineEditModel( + model, + new InlineEditWithChanges( + new StringText(''), + new TextEdit([]), + model.primaryPosition.get(), + renderExplicitly, + inlineCompletion.source.inlineCompletions.commands ?? [], + inlineCompletion.inlineCompletion + ), + tabAction, + ); + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts index fe29ffa062b..dad7dacaf49 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { equalsIfDefined, itemEquals } from '../../../../../../base/common/equals.js'; +import { BugIndicatingError } from '../../../../../../base/common/errors.js'; import { Event } from '../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { autorunWithStore, derived, derivedObservableWithCache, derivedOpts, derivedWithStore, IObservable, IReader, ISettableObservable, mapObservableArrayCached, observableValue } from '../../../../../../base/common/observable.js'; -import { localize } from '../../../../../../nls.js'; +import { autorunWithStore, derived, derivedOpts, derivedWithStore, IObservable, IReader, ISettableObservable, mapObservableArrayCached, observableValue } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; -import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { ObservableCodeEditor, observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../../common/core/lineRange.js'; import { Position } from '../../../../../common/core/position.js'; @@ -19,47 +19,62 @@ import { AbstractText, SingleTextEdit, StringText } from '../../../../../common/ import { TextLength } from '../../../../../common/core/textLength.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; import { TextModel } from '../../../../../common/model/textModel.js'; -import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; import { InlineEditsGutterIndicator } from './components/gutterIndicatorView.js'; -import { IInlineEditsIndicatorState, InlineEditsIndicator } from './components/indicatorView.js'; +import { InlineEditsIndicator } from './components/indicatorView.js'; import { InlineEditWithChanges } from './inlineEditWithChanges.js'; -import { IInlineEditsViewHost } from './inlineEditsViewInterface.js'; +import { GhostTextIndicator, InlineEditModel } from './inlineEditsModel.js'; +import { IInlineEditModel, InlineEditTabAction } from './inlineEditsViewInterface.js'; import { InlineEditsDeletionView } from './inlineEditsViews/inlineEditsDeletionView.js'; import { InlineEditsInsertionView } from './inlineEditsViews/inlineEditsInsertionView.js'; import { InlineEditsLineReplacementView } from './inlineEditsViews/inlineEditsLineReplacementView.js'; import { InlineEditsSideBySideView } from './inlineEditsViews/inlineEditsSideBySideView.js'; import { InlineEditsWordReplacementView } from './inlineEditsViews/inlineEditsWordReplacementView.js'; import { IOriginalEditorInlineDiffViewState, OriginalEditorInlineDiffView } from './inlineEditsViews/originalEditorInlineDiffView.js'; -import { applyEditToModifiedRangeMappings, createReindentEdit, InlineEditTabAction } from './utils/utils.js'; +import { applyEditToModifiedRangeMappings, createReindentEdit } from './utils/utils.js'; import './view.css'; export class InlineEditsView extends Disposable { - private readonly _editorObs = observableCodeEditor(this._editor); + private readonly _editorObs: ObservableCodeEditor = observableCodeEditor(this._editor); - private readonly _useMixedLinesDiff = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.useMixedLinesDiff); - private readonly _useInterleavedLinesDiff = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.useInterleavedLinesDiff); - private readonly _useCodeShifting = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.codeShifting); - private readonly _renderSideBySide = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.renderSideBySide); - private readonly _showCollapsed = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.showCollapsed); - private readonly _useMultiLineGhostText = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.useMultiLineGhostText); + private readonly _useMixedLinesDiff; + private readonly _useInterleavedLinesDiff; + private readonly _useCodeShifting; + private readonly _renderSideBySide; + private readonly _showCollapsed; + private readonly _useMultiLineGhostText; + + private readonly _tabAction = derived(reader => this._model.read(reader)?.tabAction.read(reader) ?? InlineEditTabAction.Inactive); private _previousView: { id: string; view: ReturnType; userJumpedToIt: boolean; editorWidth: number; + timestamp: number; } | undefined; constructor( private readonly _editor: ICodeEditor, - private readonly _edit: IObservable, - private readonly _model: IObservable, + private readonly _model: IObservable, + private readonly _ghostTextIndicator: IObservable, private readonly _focusIsInMenu: ISettableObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); + this._useMixedLinesDiff = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.useMixedLinesDiff); + this._useInterleavedLinesDiff = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.useInterleavedLinesDiff); + this._useCodeShifting = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.allowCodeShifting); + this._renderSideBySide = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.renderSideBySide); + this._showCollapsed = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.showCollapsed); + this._useMultiLineGhostText = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.useMultiLineGhostText); + this._register(autorunWithStore((reader, store) => { + const model = this._model.read(reader); + if (!model) { + return; + } + store.add( Event.any( this._sideBySide.onDidClick, @@ -69,8 +84,10 @@ export class InlineEditsView extends Disposable { ...this._wordReplacementViews.read(reader).map(w => w.onDidClick), this._inlineDiffView.onDidClick, )(e => { - e.preventDefault(); - this._host.accept(); + if (this._viewHasBeenShownLongerThan(350)) { + e.preventDefault(); + model.accept(); + } }) ); })); @@ -89,35 +106,36 @@ export class InlineEditsView extends Disposable { newTextLineCount: number; originalDisplayRange: LineRange; } | undefined>(this, reader => { - const edit = this._edit.read(reader); - if (!edit) { + const model = this._model.read(reader); + if (!model) { return undefined; } - this._model.get()?.handleInlineEditShown(edit.inlineCompletion); + model.handleInlineEditShown(); - let mappings = RangeMapping.fromEdit(edit.edit); - let newText = edit.edit.apply(edit.originalText); - let diff = lineRangeMappingFromRangeMappings(mappings, edit.originalText, new StringText(newText)); + const inlineEdit = model.inlineEdit; + let mappings = RangeMapping.fromEdit(inlineEdit.edit); + let newText = inlineEdit.edit.apply(inlineEdit.originalText); + let diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, new StringText(newText)); - const originalDisplayRange = edit.originalText.lineRange.intersect( - edit.originalLineRange.join( - LineRange.ofLength(edit.originalLineRange.startLineNumber, edit.lineEdit.newLines.length) + const originalDisplayRange = inlineEdit.originalText.lineRange.intersect( + inlineEdit.originalLineRange.join( + LineRange.ofLength(inlineEdit.originalLineRange.startLineNumber, inlineEdit.lineEdit.newLines.length) ) )!; - let state = this.determineRenderState(edit, reader, diff, new StringText(newText), originalDisplayRange); + let state = this.determineRenderState(model, reader, diff, new StringText(newText), originalDisplayRange); if (!state) { - this._model.get()?.stop(); + model.abort(`unable to determine view: tried to render ${this._previousView?.view}`); return undefined; } if (state.kind === 'sideBySide') { - const indentationAdjustmentEdit = createReindentEdit(newText, edit.modifiedLineRange); + const indentationAdjustmentEdit = createReindentEdit(newText, inlineEdit.modifiedLineRange); newText = indentationAdjustmentEdit.applyToString(newText); mappings = applyEditToModifiedRangeMappings(mappings, indentationAdjustmentEdit); - diff = lineRangeMappingFromRangeMappings(mappings, edit.originalText, new StringText(newText)); + diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, new StringText(newText)); } this._previewTextModel.setLanguage(this._editor.getModel()!.getLanguageId()); @@ -128,16 +146,16 @@ export class InlineEditsView extends Disposable { this._previewTextModel.setValue(newText); } - if (this._showCollapsed.read(reader) && this._host.tabAction.read(reader) !== InlineEditTabAction.Accept && !this._indicator.read(reader)?.isHoverVisible.read(reader) && !this._model.get()!.inAcceptFlow.read(reader)) { + if (this._showCollapsed.read(reader) && model.tabAction.read(reader) !== InlineEditTabAction.Accept && !this._indicator.read(reader)?.isHoverVisible.read(reader) && !model.inAcceptFlow.read(reader)) { state = { kind: 'hidden' }; } return { state, diff, - edit, + edit: inlineEdit, newText, - newTextLineCount: edit.modifiedLineRange.length, + newTextLineCount: inlineEdit.modifiedLineRange.length, originalDisplayRange: originalDisplayRange, }; }); @@ -150,37 +168,6 @@ export class InlineEditsView extends Disposable { null )); - // TODO: This has become messy, should it be passed in to the InlineEditsView? Maybe include in accept flow? - private readonly _host: IInlineEditsViewHost = { - displayName: derivedObservableWithCache(this, (reader, previousDisplayName) => { - const state = this._model.read(reader)?.inlineEditState; - const item = state?.read(reader); - const completionSource = item?.inlineCompletion?.source; - // TODO: expose the provider (typed) and expose the provider the edit belongs to typing and get correct edit - return (completionSource?.inlineCompletions as any)?.edits?.[0]?.provider?.displayName ?? previousDisplayName - ?? completionSource?.provider.displayName ?? localize('inlineEdit', "Inline Edit"); - }), - tabAction: derived(this, reader => { - const m = this._model.read(reader); - if (this._editorObs.isFocused.read(reader)) { - if (m && m.tabShouldJumpToInlineEdit.read(reader)) { return InlineEditTabAction.Jump; } - if (m && m.tabShouldAcceptInlineEdit.read(reader)) { return InlineEditTabAction.Accept; } - if (m && m.inlineCompletionState.read(reader)?.inlineCompletion?.sourceInlineCompletion.showInlineEditMenu) { return InlineEditTabAction.Accept; } - } - return InlineEditTabAction.Inactive; - }), - action: this._model.map((m, r) => m?.state.read(r)?.inlineCompletion?.inlineCompletion.action), - extensionCommands: this._model.map((m, r) => m?.state.read(r)?.inlineCompletion?.source.inlineCompletions.commands ?? []), - accept: () => { - this._model.get()?.accept(); - }, - jump: () => { - this._model.get()?.jump(); - } - }; - - private readonly _useGutterIndicator = observableCodeEditor(this._editor).getOption(EditorOption.inlineSuggest).map(s => s.edits.useGutterIndicator); - private readonly _indicatorCyclicDependencyCircuitBreaker = observableValue(this, false); protected readonly _indicator = derivedWithStore(this, (reader, store) => { @@ -189,9 +176,9 @@ export class InlineEditsView extends Disposable { } const indicatorDisplayRange = derivedOpts({ owner: this, equalsFn: equalsIfDefined(itemEquals()) }, reader => { - const s = this._model.read(reader)?.inlineCompletionState.read(reader); - if (s && s.inlineCompletion?.sourceInlineCompletion.showInlineEditMenu) { - return LineRange.ofLength(s.primaryGhostText.lineNumber, 1); + const ghostTextIndicator = this._ghostTextIndicator.read(reader); + if (ghostTextIndicator) { + return ghostTextIndicator.lineRange; } const state = this._uiState.read(reader); @@ -201,29 +188,29 @@ export class InlineEditsView extends Disposable { return state?.originalDisplayRange; }); - if (this._useGutterIndicator.read(reader)) { - return store.add(this._instantiationService.createInstance( - InlineEditsGutterIndicator, - this._editorObs, - indicatorDisplayRange, - this._gutterIndicatorOffset, - this._host, - this._inlineEditsIsHovered, - this._focusIsInMenu, - )); - } else { - return store.add(new InlineEditsIndicator( - this._editorObs, - derived(reader => { - const state = this._uiState.read(reader); - const range = indicatorDisplayRange.read(reader); - if (!state || !state.state || !range) { return undefined; } - const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader) + this._gutterIndicatorOffset.read(reader); - return { editTop: top, showAlways: state.state.kind !== 'sideBySide' }; - }), - this._model, - )); - } + const modelWithGhostTextSupport = derived(this, reader => { + const model = this._model.read(reader); + if (model) { + return model; + } + + const ghostTextIndicator = this._ghostTextIndicator.read(reader); + if (ghostTextIndicator) { + return ghostTextIndicator.model; + } + + return model; + }); + + return store.add(this._instantiationService.createInstance( + InlineEditsGutterIndicator, + this._editorObs, + indicatorDisplayRange, + this._gutterIndicatorOffset, + modelWithGhostTextSupport, + this._inlineEditsIsHovered, + this._focusIsInMenu, + )); }); private readonly _inlineEditsIsHovered = derived(this, reader => { @@ -245,24 +232,24 @@ export class InlineEditsView extends Disposable { private readonly _sideBySide = this._register(this._instantiationService.createInstance(InlineEditsSideBySideView, this._editor, - this._edit, + this._model.map(m => m?.inlineEdit), this._previewTextModel, this._uiState.map(s => s && s.state?.kind === 'sideBySide' ? ({ edit: s.edit, newTextLineCount: s.newTextLineCount, originalDisplayRange: s.originalDisplayRange, }) : undefined), - this._host, + this._tabAction, )); protected readonly _deletion = this._register(this._instantiationService.createInstance(InlineEditsDeletionView, this._editor, - this._edit, + this._model.map(m => m?.inlineEdit), this._uiState.map(s => s && s.state?.kind === 'deletion' ? ({ originalRange: s.state.originalRange, deletions: s.state.deletions, }) : undefined), - this._host, + this._tabAction, )); protected readonly _insertion = this._register(this._instantiationService.createInstance(InlineEditsInsertionView, @@ -272,7 +259,7 @@ export class InlineEditsView extends Disposable { startColumn: s.state.column, text: s.state.text, }) : undefined), - this._host, + this._tabAction, )); private readonly _inlineDiffViewState = derived(this, reader => { @@ -292,7 +279,7 @@ export class InlineEditsView extends Disposable { protected readonly _inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel)); protected readonly _wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => { - return store.add(this._instantiationService.createInstance(InlineEditsWordReplacementView, this._editorObs, e, [e], this._host)); + return store.add(this._instantiationService.createInstance(InlineEditsWordReplacementView, this._editorObs, e, [e], this._tabAction)); }); protected readonly _lineReplacementView = this._register(this._instantiationService.createInstance(InlineEditsLineReplacementView, @@ -303,21 +290,23 @@ export class InlineEditsView extends Disposable { modifiedLines: s.state.modifiedLines, replacements: s.state.replacements, }) : undefined), - this._host + this._tabAction, )); - private getCacheId(edit: InlineEditWithChanges) { - if (this._model.get()?.inAcceptPartialFlow.get()) { - return `${edit.inlineCompletion.id}_${edit.edit.edits.map(edit => edit.range.toString() + edit.text).join(',')}`; + private getCacheId(model: IInlineEditModel) { + const inlineEdit = model.inlineEdit; + if (model.inPartialAcceptFlow.get()) { + return `${inlineEdit.inlineCompletion.id}_${inlineEdit.edit.edits.map(innerEdit => innerEdit.range.toString() + innerEdit.text).join(',')}`; } - return edit.inlineCompletion.id; + return inlineEdit.inlineCompletion.id; } - private determineView(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange): string { + private determineView(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange): string { // Check if we can use the previous view if it is the same InlineCompletion as previously shown - const canUseCache = this._previousView?.id === this.getCacheId(edit); - const reconsiderViewAfterJump = edit.userJumpedToIt !== this._previousView?.userJumpedToIt && + const inlineEdit = model.inlineEdit; + const canUseCache = this._previousView?.id === this.getCacheId(model); + const reconsiderViewAfterJump = inlineEdit.userJumpedToIt !== this._previousView?.userJumpedToIt && ( (this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible' && this._previousView?.view !== 'mixedLines') || (this._useInterleavedLinesDiff.read(reader) === 'afterJump' && this._previousView?.view !== 'interleavedLines') @@ -339,36 +328,36 @@ export class InlineEditsView extends Disposable { if ( isSingleInnerEdit && ( this._useMixedLinesDiff.read(reader) === 'forStableInsertions' - && this._useCodeShifting.read(reader) - && isSingleLineInsertionAfterPosition(diff, edit.cursorPosition) + && this._useCodeShifting.read(reader) !== 'never' + && isSingleLineInsertionAfterPosition(diff, inlineEdit.cursorPosition) ) ) { return 'insertionInline'; } - const innerValues = inner.map(m => ({ original: edit.originalText.getValueOfRange(m.originalRange), modified: newText.getValueOfRange(m.modifiedRange) })); + const innerValues = inner.map(m => ({ original: inlineEdit.originalText.getValueOfRange(m.originalRange), modified: newText.getValueOfRange(m.modifiedRange) })); if (innerValues.every(({ original, modified }) => modified.trim() === '' && original.length > 0 && (original.length > modified.length || original.trim() !== ''))) { return 'deletion'; } - if (isSingleMultiLineInsertion(diff) && this._useMultiLineGhostText.read(reader) && this._useCodeShifting.read(reader)) { + if (isSingleMultiLineInsertion(diff) && this._useMultiLineGhostText.read(reader) && this._useCodeShifting.read(reader) === 'always') { return 'insertionMultiLine'; } - const numOriginalLines = edit.originalLineRange.length; - const numModifiedLines = edit.modifiedLineRange.length; + const numOriginalLines = inlineEdit.originalLineRange.length; + const numModifiedLines = inlineEdit.modifiedLineRange.length; const allInnerChangesNotTooLong = inner.every(m => TextLength.ofRange(m.originalRange).columnCount < InlineEditsWordReplacementView.MAX_LENGTH && TextLength.ofRange(m.modifiedRange).columnCount < InlineEditsWordReplacementView.MAX_LENGTH); if (allInnerChangesNotTooLong && isSingleInnerEdit && numOriginalLines === 1 && numModifiedLines === 1) { // Make sure there is no insertion, even if we grow them if ( !inner.some(m => m.originalRange.isEmpty()) || - !growEditsUntilWhitespace(inner.map(m => new SingleTextEdit(m.originalRange, '')), edit.originalText).some(e => e.range.isEmpty() && TextLength.ofRange(e.range).columnCount < InlineEditsWordReplacementView.MAX_LENGTH) + !growEditsUntilWhitespace(inner.map(m => new SingleTextEdit(m.originalRange, '')), inlineEdit.originalText).some(e => e.range.isEmpty() && TextLength.ofRange(e.range).columnCount < InlineEditsWordReplacementView.MAX_LENGTH) ) { return 'wordReplacements'; } } if (numOriginalLines > 0 && numModifiedLines > 0) { - if (this._renderSideBySide.read(reader) !== 'never' && InlineEditsSideBySideView.fitsInsideViewport(this._editor, edit, originalDisplayRange, reader)) { + if (this._renderSideBySide.read(reader) !== 'never' && InlineEditsSideBySideView.fitsInsideViewport(this._editor, inlineEdit, originalDisplayRange, reader)) { return 'sideBySide'; } @@ -376,24 +365,25 @@ export class InlineEditsView extends Disposable { } if ( - (this._useMixedLinesDiff.read(reader) === 'whenPossible' || (edit.userJumpedToIt && this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible')) + (this._useMixedLinesDiff.read(reader) === 'whenPossible' || (inlineEdit.userJumpedToIt && this._useMixedLinesDiff.read(reader) === 'afterJumpWhenPossible')) && diff.every(m => OriginalEditorInlineDiffView.supportsInlineDiffRendering(m)) ) { return 'mixedLines'; } - if (this._useInterleavedLinesDiff.read(reader) === 'always' || (edit.userJumpedToIt && this._useInterleavedLinesDiff.read(reader) === 'afterJump')) { + if (this._useInterleavedLinesDiff.read(reader) === 'always' || (inlineEdit.userJumpedToIt && this._useInterleavedLinesDiff.read(reader) === 'afterJump')) { return 'interleavedLines'; } return 'sideBySide'; } - private determineRenderState(edit: InlineEditWithChanges, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange) { + private determineRenderState(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText, originalDisplayRange: LineRange) { + const inlineEdit = model.inlineEdit; - const view = this.determineView(edit, reader, diff, newText, originalDisplayRange); + const view = this.determineView(model, reader, diff, newText, originalDisplayRange); - this._previousView = { id: this.getCacheId(edit), view, userJumpedToIt: edit.userJumpedToIt, editorWidth: this._editor.getLayoutInfo().width }; + this._previousView = { id: this.getCacheId(model), view, userJumpedToIt: inlineEdit.userJumpedToIt, editorWidth: this._editor.getLayoutInfo().width, timestamp: Date.now() }; switch (view) { case 'insertionInline': return { kind: 'insertionInline' as const }; @@ -408,7 +398,7 @@ export class InlineEditsView extends Disposable { if (view === 'deletion') { return { kind: 'deletion' as const, - originalRange: edit.originalLineRange, + originalRange: inlineEdit.originalLineRange, deletions: inner.map(m => m.originalRange), }; } @@ -429,10 +419,10 @@ export class InlineEditsView extends Disposable { } if (view === 'wordReplacements') { - let grownEdits = growEditsToEntireWord(replacements, edit.originalText); + let grownEdits = growEditsToEntireWord(replacements, inlineEdit.originalText); if (grownEdits.some(e => e.range.isEmpty())) { - grownEdits = growEditsUntilWhitespace(replacements, edit.originalText); + grownEdits = growEditsUntilWhitespace(replacements, inlineEdit.originalText); } return { @@ -444,15 +434,25 @@ export class InlineEditsView extends Disposable { if (view === 'lineReplacement') { return { kind: 'lineReplacement' as const, - originalRange: edit.originalLineRange, - modifiedRange: edit.modifiedLineRange, - modifiedLines: edit.modifiedLineRange.mapToLineArray(line => newText.getLineAt(line)), + originalRange: inlineEdit.originalLineRange, + modifiedRange: inlineEdit.modifiedLineRange, + modifiedLines: inlineEdit.modifiedLineRange.mapToLineArray(line => newText.getLineAt(line)), replacements: inner.map(m => ({ originalRange: m.originalRange, modifiedRange: m.modifiedRange })), }; } return undefined; } + + private _viewHasBeenShownLongerThan(durationMs: number): boolean { + const viewCreationTime = this._previousView?.timestamp; + if (!viewCreationTime) { + throw new BugIndicatingError('viewHasBeenShownLongThan called before a view has been shown'); + } + + const currentTime = Date.now(); + return (currentTime - viewCreationTime) >= durationMs; + } } function isSingleLineInsertionAfterPosition(diff: DetailedLineRangeMapping[], position: Position | null) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts index ffa4cdbafff..a00fc94d390 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts @@ -7,18 +7,31 @@ import { IMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; import { Event } from '../../../../../../base/common/event.js'; import { IObservable } from '../../../../../../base/common/observable.js'; import { Command } from '../../../../../common/languages.js'; -import { InlineEditTabAction } from './utils/utils.js'; +import { InlineEditWithChanges } from './inlineEditWithChanges.js'; + +export enum InlineEditTabAction { + Jump = 'jump', + Accept = 'accept', + Inactive = 'inactive' +} export interface IInlineEditsView { isHovered: IObservable; onDidClick: Event; } -export interface IInlineEditsViewHost { - displayName: IObservable; - action: IObservable; +export interface IInlineEditModel { + displayName: string; + action: Command | undefined; + extensionCommands: Command[]; + inlineEdit: InlineEditWithChanges; + tabAction: IObservable; - extensionCommands: IObservable; + inAcceptFlow: IObservable; + inPartialAcceptFlow: IObservable; + + handleInlineEditShown(): void; accept(): void; jump(): void; + abort(reason: string): void; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts similarity index 55% rename from src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts rename to src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts index 9b5fcbb5159..ad651d80219 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/viewAndDiffProducer.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts @@ -8,17 +8,23 @@ import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { derived, IObservable, ISettableObservable } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; +import { ObservableCodeEditor, observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; import { Range } from '../../../../../common/core/range.js'; import { SingleTextEdit, TextEdit } from '../../../../../common/core/textEdit.js'; import { TextModelText } from '../../../../../common/model/textModelText.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; import { InlineEdit } from '../../model/inlineEdit.js'; import { InlineEditWithChanges } from './inlineEditWithChanges.js'; +import { GhostTextIndicator, InlineEditModel } from './inlineEditsModel.js'; import { InlineEditsView } from './inlineEditsView.js'; +import { InlineEditTabAction } from './inlineEditsViewInterface.js'; export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This class is no longer a diff producer. Rename it or get rid of it public static readonly hot = createHotClass(InlineEditsViewAndDiffProducer); + private readonly _editorObs: ObservableCodeEditor; + private readonly _inlineEdit = derived(this, (reader) => { const model = this._model.read(reader); if (!model) { return undefined; } @@ -30,7 +36,7 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c const editOffset = model.inlineEditState.get()?.inlineCompletion.updatedEdit.read(reader); if (!editOffset) { return undefined; } - const offsetEdits = model.inAcceptPartialFlow.read(reader) ? [editOffset.edits[0]] : editOffset.edits; + const offsetEdits = model.inPartialAcceptFlow.read(reader) ? [editOffset.edits[0]] : editOffset.edits; const edits = offsetEdits.map(e => { const innerEditRange = Range.fromPositions( textModel.getPositionAt(e.replaceRange.start), @@ -42,7 +48,42 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c const diffEdits = new TextEdit(edits); const text = new TextModelText(textModel); - return new InlineEditWithChanges(text, diffEdits, model.primaryPosition.get(), inlineEdit.renderExplicitly, inlineEdit.commands, inlineEdit.inlineCompletion); + return new InlineEditWithChanges(text, diffEdits, model.primaryPosition.get(), inlineEdit.jumpedTo, inlineEdit.commands, inlineEdit.inlineCompletion); + }); + + private readonly _inlineEditModel = derived(this, reader => { + const model = this._model.read(reader); + if (!model) { return undefined; } + const edit = this._inlineEdit.read(reader); + if (!edit) { return undefined; } + + const tabAction = derived(this, reader => { + if (this._editorObs.isFocused.read(reader)) { + if (model.tabShouldJumpToInlineEdit.read(reader)) { return InlineEditTabAction.Jump; } + if (model.tabShouldAcceptInlineEdit.read(reader)) { return InlineEditTabAction.Accept; } + } + return InlineEditTabAction.Inactive; + }); + + return new InlineEditModel(model, edit, tabAction); + }); + + private readonly _ghostTextIndicator = derived(this, reader => { + const model = this._model.read(reader); + if (!model) { return undefined; } + const state = model.inlineCompletionState.read(reader); + if (!state) { return undefined; } + const inlineCompletion = state.inlineCompletion; + if (!inlineCompletion) { return undefined; } + + if (!inlineCompletion.sourceInlineCompletion.showInlineEditMenu) { + return undefined; + } + + const lineRange = LineRange.ofLength(state.primaryGhostText.lineNumber, 1); + const renderExplicitly = false; + + return new GhostTextIndicator(this._editor, model, lineRange, inlineCompletion, renderExplicitly); }); constructor( @@ -50,10 +91,12 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c private readonly _edit: IObservable, private readonly _model: IObservable, private readonly _focusIsInMenu: ISettableObservable, - @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); - this._register(this._instantiationService.createInstance(InlineEditsView, this._editor, this._inlineEdit, this._model, this._focusIsInMenu)); + this._editorObs = observableCodeEditor(this._editor); + + this._register(instantiationService.createInstance(InlineEditsView, this._editor, this._inlineEditModel, this._ghostTextIndicator, this._focusIsInMenu)); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts index f8c6bcf3934..9feed6f4a6b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts @@ -9,22 +9,28 @@ import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { constObservable, derived, derivedObservableWithCache, IObservable } from '../../../../../../../base/common/observable.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { ICodeEditor } from '../../../../../../browser/editorBrowser.js'; -import { observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; +import { ObservableCodeEditor, observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; import { Point } from '../../../../../../browser/point.js'; import { LineRange } from '../../../../../../common/core/lineRange.js'; import { Position } from '../../../../../../common/core/position.js'; import { Range } from '../../../../../../common/core/range.js'; -import { IInlineEditsView, IInlineEditsViewHost } from '../inlineEditsViewInterface.js'; +import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { InlineEditWithChanges } from '../inlineEditWithChanges.js'; import { getOriginalBorderColor, originalBackgroundColor } from '../theme.js'; import { createRectangle, getPrefixTrim, mapOutFalsy, maxContentWidthInRange } from '../utils/utils.js'; export class InlineEditsDeletionView extends Disposable implements IInlineEditsView { - private readonly _editorObs = observableCodeEditor(this._editor); private readonly _onDidClick = this._register(new Emitter()); readonly onDidClick = this._onDidClick.event; + private readonly _editorObs: ObservableCodeEditor; + + private readonly _originalVerticalStartPosition: IObservable; + private readonly _originalVerticalEndPosition: IObservable; + + private readonly _originalDisplayRange: IObservable; + constructor( private readonly _editor: ICodeEditor, private readonly _edit: IObservable, @@ -32,10 +38,26 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV originalRange: LineRange; deletions: Range[]; } | undefined>, - private readonly _host: IInlineEditsViewHost, + private readonly _tabAction: IObservable, ) { super(); + this._editorObs = observableCodeEditor(this._editor); + + const originalStartPosition = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + return inlineEdit ? new Position(inlineEdit.originalLineRange.startLineNumber, 1) : null; + }); + + const originalEndPosition = derived(this, (reader) => { + const inlineEdit = this._edit.read(reader); + return inlineEdit ? new Position(inlineEdit.originalLineRange.endLineNumberExclusive, 1) : null; + }); + + this._originalDisplayRange = this._uiState.map(s => s?.originalRange); + this._originalVerticalStartPosition = this._editorObs.observePosition(originalStartPosition, this._store).map(p => p?.y); + this._originalVerticalEndPosition = this._editorObs.observePosition(originalEndPosition, this._store).map(p => p?.y); + this._register(this._editorObs.createOverlayWidget({ domNode: this._nonOverflowView.element, position: constObservable(null), @@ -50,20 +72,6 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV private readonly _display = derived(this, reader => !!this._uiState.read(reader) ? 'block' : 'none'); - private readonly _originalStartPosition = derived(this, (reader) => { - const inlineEdit = this._edit.read(reader); - return inlineEdit ? new Position(inlineEdit.originalLineRange.startLineNumber, 1) : null; - }); - - private readonly _originalEndPosition = derived(this, (reader) => { - const inlineEdit = this._edit.read(reader); - return inlineEdit ? new Position(inlineEdit.originalLineRange.endLineNumberExclusive, 1) : null; - }); - - private readonly _originalVerticalStartPosition = this._editorObs.observePosition(this._originalStartPosition, this._store).map(p => p?.y); - private readonly _originalVerticalEndPosition = this._editorObs.observePosition(this._originalEndPosition, this._store).map(p => p?.y); - - private readonly _originalDisplayRange = this._uiState.map(s => s?.originalRange); private readonly _editorMaxContentWidthInRange = derived(this, reader => { const originalDisplayRange = this._originalDisplayRange.read(reader); if (!originalDisplayRange) { @@ -151,7 +159,7 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV { hideLeft: layoutInfo.horizontalScrollOffset !== 0 } ); - const originalBorderColor = getOriginalBorderColor(this._host.tabAction).read(reader); + const originalBorderColor = getOriginalBorderColor(this._tabAction).read(reader); return [ n.svgElem('path', { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts index 494210fa6c7..bb20b7d863e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts @@ -10,7 +10,7 @@ import { constObservable, derived, derivedWithStore, IObservable, observableValu import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { ICodeEditor } from '../../../../../../browser/editorBrowser.js'; -import { observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; +import { ObservableCodeEditor, observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; import { Rect } from '../../../../../../browser/rect.js'; import { LineSource, renderLines, RenderOptions } from '../../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; @@ -23,12 +23,12 @@ import { TokenArray } from '../../../../../../common/tokens/tokenArray.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel.js'; import { GhostText, GhostTextPart } from '../../../model/ghostText.js'; import { GhostTextView } from '../../ghostText/ghostTextView.js'; -import { IInlineEditsView, IInlineEditsViewHost } from '../inlineEditsViewInterface.js'; +import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { getModifiedBorderColor, modifiedChangedLineBackgroundColor } from '../theme.js'; import { createRectangle, getPrefixTrim, mapOutFalsy } from '../utils/utils.js'; export class InlineEditsInsertionView extends Disposable implements IInlineEditsView { - private readonly _editorObs = observableCodeEditor(this._editor); + private readonly _editorObs: ObservableCodeEditor; private readonly _onDidClick = this._register(new Emitter()); readonly onDidClick = this._onDidClick.event; @@ -86,18 +86,8 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits return new GhostText(state.lineNumber, [new GhostTextPart(state.column, state.text, false, inlineDecorations)]); }); - protected readonly _ghostTextView = this._register(this._instantiationService.createInstance(GhostTextView, - this._editor, - { - ghostText: this._ghostText, - minReservedLineCount: constObservable(0), - targetTextModel: this._editorObs.model.map(model => model ?? undefined), - warning: constObservable(undefined), - }, - observableValue(this, { syntaxHighlightingEnabled: true, extraClasses: ['inline-edit'] }), - true, - true - )); + protected readonly _ghostTextView: GhostTextView; + readonly isHovered: IObservable; constructor( private readonly _editor: ICodeEditor, @@ -106,12 +96,29 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits startColumn: number; text: string; } | undefined>, - private readonly _host: IInlineEditsViewHost, - @IInstantiationService private readonly _instantiationService: IInstantiationService, + private readonly _tabAction: IObservable, + @IInstantiationService instantiationService: IInstantiationService, @ILanguageService private readonly _languageService: ILanguageService, ) { super(); + this._editorObs = observableCodeEditor(this._editor); + + this._ghostTextView = this._register(instantiationService.createInstance(GhostTextView, + this._editor, + { + ghostText: this._ghostText, + minReservedLineCount: constObservable(0), + targetTextModel: this._editorObs.model.map(model => model ?? undefined), + warning: constObservable(undefined), + }, + observableValue(this, { syntaxHighlightingEnabled: true, extraClasses: ['inline-edit'] }), + true, + true + )); + + this.isHovered = this._ghostTextView.isHovered; + this._register(this._ghostTextView.onDidClick((e) => { this._onDidClick.fire(e); })); @@ -210,7 +217,7 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits const right = editorLayout.contentLeft + this._editorMaxContentWidthInRange.read(reader) - horizontalScrollOffset; const prefixLeftOffset = this._maxPrefixTrim.read(reader).prefixLeftOffset ?? 0 /* fix due to observable bug? */; - const left = editorLayout.contentLeft + prefixLeftOffset; + const left = editorLayout.contentLeft + prefixLeftOffset - horizontalScrollOffset; if (right <= left) { return null; } @@ -222,14 +229,14 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits const top = this._editor.getTopForLineNumber(state.lineNumber) - scrollTop + topTrim; const bottom = top + height; - const PADDING = 3; - const overlay = new Rect(left, top, right, bottom).withMargin(PADDING); + const overlay = new Rect(left, top, right, bottom); return { overlay, - horizontalScrollOffset, + contentLeft: editorLayout.contentLeft, minContentWidthRequired: prefixLeftOffset + overlay.width + verticalScrollbarWidth, borderRadius: 4, + padding: 3 }; }).recomputeInitiallyAndOnChange(this._store); @@ -241,19 +248,21 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits if (!overlayLayoutObs) { return undefined; } const layoutInfo = overlayLayoutObs.read(reader); + const overlay = layoutInfo.overlay; + const croppedOverlay = new Rect(Math.max(overlay.left, layoutInfo.contentLeft), overlay.top, overlay.right, overlay.bottom); const rectangleOverlay = createRectangle( { - topLeft: layoutInfo.overlay.getLeftTop(), - width: layoutInfo.overlay.width + 1, - height: layoutInfo.overlay.height + 1, + topLeft: croppedOverlay.getLeftTop(), + width: croppedOverlay.width + 1, + height: croppedOverlay.height + 1, }, - 0, + layoutInfo.padding, layoutInfo.borderRadius, - { hideLeft: layoutInfo.horizontalScrollOffset !== 0 } + { hideLeft: croppedOverlay.left !== overlay.left } ); - const modifiedBorderColor = getModifiedBorderColor(this._host.tabAction).read(reader); + const modifiedBorderColor = getModifiedBorderColor(this._tabAction).read(reader); return [ n.svgElem('path', { @@ -281,6 +290,4 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits }, [ [this._foregroundSvg], ]).keepUpdated(this._store); - - readonly isHovered = this._ghostTextView.isHovered; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts index d952cda8a05..eb7460eeed2 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts @@ -25,7 +25,7 @@ import { IModelDecorationOptions, TrackedRangeStickiness } from '../../../../../ import { LineTokens } from '../../../../../../common/tokens/lineTokens.js'; import { TokenArray } from '../../../../../../common/tokens/tokenArray.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel.js'; -import { IInlineEditsView, IInlineEditsViewHost } from '../inlineEditsViewInterface.js'; +import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { getModifiedBorderColor, modifiedChangedLineBackgroundColor } from '../theme.js'; import { getPrefixTrim, mapOutFalsy, rectToProps } from '../utils/utils.js'; import { rangesToBubbleRanges, Replacement } from './inlineEditsWordReplacementView.js'; @@ -153,7 +153,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin }); private readonly _viewZoneInfo = derived<{ height: number; lineNumber: number } | undefined>(reader => { - const shouldShowViewZone = this._editor.getOption(EditorOption.inlineSuggest).map(o => o.edits.codeShifting).read(reader); + const shouldShowViewZone = this._editor.getOption(EditorOption.inlineSuggest).map(o => o.edits.allowCodeShifting === 'always').read(reader); if (!shouldShowViewZone) { return undefined; } @@ -197,7 +197,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin l.style.position = 'relative'; }); - const modifiedBorderColor = getModifiedBorderColor(this._host.tabAction).read(reader); + const modifiedBorderColor = getModifiedBorderColor(this._tabAction).read(reader); return [ n.div({ @@ -233,7 +233,10 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin cursor: 'pointer', pointerEvents: 'auto', }, - onmouseup: (e) => this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)), + onmousedown: e => { + e.preventDefault(); // This prevents that the editor loses focus + }, + onclick: (e) => this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)), }, [ n.div({ style: { @@ -284,7 +287,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin modifiedLines: string[]; replacements: Replacement[]; } | undefined>, - private readonly _host: IInlineEditsViewHost, + private readonly _tabAction: IObservable, @ILanguageService private readonly _languageService: ILanguageService ) { super(); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts index 12648f46114..41ff1f50b23 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts @@ -23,7 +23,7 @@ import { Range } from '../../../../../../common/core/range.js'; import { ITextModel } from '../../../../../../common/model.js'; import { StickyScrollController } from '../../../../../stickyScroll/browser/stickyScrollController.js'; import { InlineCompletionContextKeys } from '../../../controller/inlineCompletionContextKeys.js'; -import { IInlineEditsView, IInlineEditsViewHost } from '../inlineEditsViewInterface.js'; +import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { InlineEditWithChanges } from '../inlineEditWithChanges.js'; import { getModifiedBorderColor, getOriginalBorderColor, modifiedBackgroundColor, originalBackgroundColor } from '../theme.js'; import { PathBuilder, createRectangle, getOffsetForPos, mapOutFalsy, maxContentWidthInRange } from '../utils/utils.js'; @@ -64,7 +64,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit newTextLineCount: number; originalDisplayRange: LineRange; } | undefined>, - private readonly _host: IInlineEditsViewHost, + private readonly _tabAction: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IThemeService private readonly _themeService: IThemeService, ) { @@ -123,8 +123,11 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit private readonly previewRef = n.ref(); private readonly _editorContainer = n.div({ - class: ['editorContainer', this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !v.edits.useGutterIndicator && 'showHover')], + class: ['editorContainer'], style: { position: 'absolute', overflow: 'hidden', cursor: 'pointer' }, + onmousedown: e => { + e.preventDefault(); // This prevents that the editor loses focus + }, onclick: (e) => { this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)); } @@ -505,8 +508,8 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit const layoutInfoObs = mapOutFalsy(this._previewEditorLayoutInfo).read(reader); if (!layoutInfoObs) { return undefined; } - const modifiedBorderColor = getModifiedBorderColor(this._host.tabAction).read(reader); - const originalBorderColor = getOriginalBorderColor(this._host.tabAction).read(reader); + const modifiedBorderColor = getModifiedBorderColor(this._tabAction).read(reader); + const originalBorderColor = getOriginalBorderColor(this._tabAction).read(reader); return [ n.svgElem('path', { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts index d3ee68c60cc..d18dad373c3 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts @@ -14,9 +14,9 @@ import { Rect } from '../../../../../../browser/rect.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; import { OffsetRange } from '../../../../../../common/core/offsetRange.js'; import { SingleTextEdit } from '../../../../../../common/core/textEdit.js'; -import { IInlineEditsView } from '../inlineEditsViewInterface.js'; +import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { getModifiedBorderColor } from '../theme.js'; -import { InlineEditTabAction, mapOutFalsy, rectToProps } from '../utils/utils.js'; +import { mapOutFalsy, rectToProps } from '../utils/utils.js'; export class InlineEditsWordInsertView extends Disposable implements IInlineEditsView { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts index 6303d795581..f7272f15052 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts @@ -7,7 +7,7 @@ import { getWindow, n, ObserverNodeWithElement } from '../../../../../../../base import { IMouseEvent, StandardMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; import { Emitter } from '../../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; -import { constObservable, derived, observableValue } from '../../../../../../../base/common/observable.js'; +import { constObservable, derived, IObservable, observableValue } from '../../../../../../../base/common/observable.js'; import { editorBackground, editorHoverForeground, scrollbarShadow } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; @@ -22,7 +22,7 @@ import { SingleTextEdit } from '../../../../../../common/core/textEdit.js'; import { ILanguageService } from '../../../../../../common/languages/language.js'; import { LineTokens } from '../../../../../../common/tokens/lineTokens.js'; import { TokenArray } from '../../../../../../common/tokens/tokenArray.js'; -import { IInlineEditsView, IInlineEditsViewHost } from '../inlineEditsViewInterface.js'; +import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { getModifiedBorderColor, modifiedChangedTextOverlayColor, originalChangedTextOverlayColor, replacementViewBackground } from '../theme.js'; import { mapOutFalsy, rectToProps } from '../utils/utils.js'; @@ -47,7 +47,7 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin /** Must be single-line in both sides */ private readonly _edit: SingleTextEdit, private readonly _innerEdits: SingleTextEdit[], - private readonly _host: IInlineEditsViewHost, + private readonly _tabAction: IObservable, @ILanguageService private readonly _languageService: ILanguageService, ) { super(); @@ -162,7 +162,7 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin const edits = layoutProps.innerEdits.map(edit => ({ modified: edit.modified.translateX(-contentLeft), original: edit.original.translateX(-contentLeft) })); - const modifiedBorderColor = getModifiedBorderColor(this._host.tabAction).read(reader); + const modifiedBorderColor = getModifiedBorderColor(this._tabAction).read(reader); return [ n.div({ @@ -186,6 +186,9 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin cursor: 'pointer', pointerEvents: 'auto', }, + onmousedown: e => { + e.preventDefault(); // This prevents that the editor loses focus + }, onmouseup: (e) => this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)), obsRef: (elem) => { this._hoverableElement.set(elem, undefined); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts index 414f455553d..1ca0f662e92 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts @@ -41,7 +41,9 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE readonly onDidClick = this._onDidClick.event; readonly isHovered = observableCodeEditor(this._originalEditor).isTargetHovered( - p => p.target.type === MouseTargetType.CONTENT_TEXT && p.target.detail.injectedText?.options.attachedData instanceof InlineEditAttachedData, + p => p.target.type === MouseTargetType.CONTENT_TEXT && + p.target.detail.injectedText?.options.attachedData instanceof InlineEditAttachedData && + p.target.detail.injectedText.options.attachedData.owner === this, this._store ); @@ -71,7 +73,7 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE return; } const a = e.target.detail.injectedText?.options.attachedData; - if (a instanceof InlineEditAttachedData) { + if (a instanceof InlineEditAttachedData && a.owner === this) { this._onDidClick.fire(e.event); } })); @@ -263,7 +265,7 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE ...extraClasses // include extraClasses for additional styling if provided ), cursorStops: InjectedTextCursorStops.None, - attachedData: new InlineEditAttachedData(), + attachedData: new InlineEditAttachedData(this), }, zIndex: 2, showIfCollapsed: true, @@ -280,6 +282,7 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE } class InlineEditAttachedData { + constructor(public readonly owner: OriginalEditorInlineDiffView) { } } function allowsTrueInlineDiffRendering(mapping: DetailedLineRangeMapping): boolean { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts index 85cc06a8a27..056fbf695d1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts @@ -8,7 +8,7 @@ import { IObservable } from '../../../../../../base/common/observable.js'; import { localize } from '../../../../../../nls.js'; import { diffRemoved, diffInsertedLine, diffInserted, editorHoverBorder, editorHoverStatusBarBackground, buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground } from '../../../../../../platform/theme/common/colorRegistry.js'; import { registerColor, transparent, asCssVariable, lighten, darken } from '../../../../../../platform/theme/common/colorUtils.js'; -import { InlineEditTabAction } from './utils/utils.js'; +import { InlineEditTabAction } from './inlineEditsViewInterface.js'; export const originalBackgroundColor = registerColor( 'inlineEdit.originalBackground', diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts index 9427e0b23a0..a393e04e241 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts @@ -26,12 +26,6 @@ import { SingleTextEdit, TextEdit } from '../../../../../../common/core/textEdit import { RangeMapping } from '../../../../../../common/diff/rangeMapping.js'; import { indentOfLine } from '../../../../../../common/model/textModel.js'; -export enum InlineEditTabAction { - Jump = 'jump', - Accept = 'accept', - Inactive = 'inactive' -} - export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): number { editor.layoutInfo.read(reader); editor.value.read(reader); diff --git a/src/vs/editor/contrib/multicursor/browser/multicursor.ts b/src/vs/editor/contrib/multicursor/browser/multicursor.ts index c3bcf55dd7c..abc43c1f7d7 100644 --- a/src/vs/editor/contrib/multicursor/browser/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/browser/multicursor.ts @@ -201,7 +201,7 @@ class InsertCursorAtEndOfLineSelected extends EditorAction { constructor() { super({ id: 'editor.action.addCursorsToBottom', - label: nls.localize2('mutlicursor.addCursorsToBottom', "Add Cursors To Bottom"), + label: nls.localize2('mutlicursor.addCursorsToBottom', "Add Cursors to Bottom"), precondition: undefined }); } @@ -233,7 +233,7 @@ class InsertCursorAtTopOfLineSelected extends EditorAction { constructor() { super({ id: 'editor.action.addCursorsToTop', - label: nls.localize2('mutlicursor.addCursorsToTop', "Add Cursors To Top"), + label: nls.localize2('mutlicursor.addCursorsToTop', "Add Cursors to Top"), precondition: undefined }); } @@ -686,7 +686,7 @@ export class AddSelectionToNextFindMatchAction extends MultiCursorSelectionContr constructor() { super({ id: 'editor.action.addSelectionToNextFindMatch', - label: nls.localize2('addSelectionToNextFindMatch', "Add Selection To Next Find Match"), + label: nls.localize2('addSelectionToNextFindMatch', "Add Selection to Next Find Match"), precondition: undefined, kbOpts: { kbExpr: EditorContextKeys.focus, @@ -710,7 +710,7 @@ export class AddSelectionToPreviousFindMatchAction extends MultiCursorSelectionC constructor() { super({ id: 'editor.action.addSelectionToPreviousFindMatch', - label: nls.localize2('addSelectionToPreviousFindMatch', "Add Selection To Previous Find Match"), + label: nls.localize2('addSelectionToPreviousFindMatch', "Add Selection to Previous Find Match"), precondition: undefined, menuOpts: { menuId: MenuId.MenubarSelectionMenu, @@ -729,7 +729,7 @@ export class MoveSelectionToNextFindMatchAction extends MultiCursorSelectionCont constructor() { super({ id: 'editor.action.moveSelectionToNextFindMatch', - label: nls.localize2('moveSelectionToNextFindMatch', "Move Last Selection To Next Find Match"), + label: nls.localize2('moveSelectionToNextFindMatch', "Move Last Selection to Next Find Match"), precondition: undefined, kbOpts: { kbExpr: EditorContextKeys.focus, @@ -747,7 +747,7 @@ export class MoveSelectionToPreviousFindMatchAction extends MultiCursorSelection constructor() { super({ id: 'editor.action.moveSelectionToPreviousFindMatch', - label: nls.localize2('moveSelectionToPreviousFindMatch', "Move Last Selection To Previous Find Match"), + label: nls.localize2('moveSelectionToPreviousFindMatch', "Move Last Selection to Previous Find Match"), precondition: undefined }); } @@ -803,7 +803,7 @@ export class CompatChangeAll extends MultiCursorSelectionControllerAction { } class SelectionHighlighterState { - private readonly _modelVersionId: number = this._model.getVersionId(); + private readonly _modelVersionId: number; private _cachedFindMatches: Range[] | null = null; constructor( @@ -813,6 +813,7 @@ class SelectionHighlighterState { private readonly _wordSeparators: string | null, prevState: SelectionHighlighterState | null ) { + this._modelVersionId = this._model.getVersionId(); if (prevState && this._model === prevState._model && this._searchText === prevState._searchText diff --git a/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts index a6e7a743458..b4f95f7158e 100644 --- a/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts @@ -9,7 +9,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IMatch } from '../../../../base/common/filters.js'; import { IPreparedQuery, pieceToQuery, prepareQuery, scoreFuzzy2 } from '../../../../base/common/fuzzyScorer.js'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { format, trim } from '../../../../base/common/strings.js'; import { IRange, Range } from '../../../common/core/range.js'; import { ScrollType } from '../../../common/editorCommon.js'; @@ -169,21 +169,21 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit const symbolsPromise = this.getDocumentSymbols(model, token); // Set initial picks and update on type - let picksCts: CancellationTokenSource | undefined = undefined; + const picksCts = disposables.add(new MutableDisposable()); const updatePickerItems = async (positionToEnclose: Position | undefined) => { // Cancel any previous ask for picks and busy - picksCts?.dispose(true); + picksCts?.value?.cancel(); picker.busy = false; // Create new cancellation source for this run - picksCts = new CancellationTokenSource(token); + picksCts.value = new CancellationTokenSource(); // Collect symbol picks picker.busy = true; try { const query = prepareQuery(picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim()); - const items = await this.doGetSymbolPicks(symbolsPromise, query, undefined, picksCts.token, model); + const items = await this.doGetSymbolPicks(symbolsPromise, query, undefined, picksCts.value.token, model); if (token.isCancellationRequested) { return; } diff --git a/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts b/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts index 01b57413029..8fda7ce5b66 100644 --- a/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts +++ b/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts @@ -8,7 +8,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../browser/editorExtensions.js'; import { EditorOption, IEditorMinimapOptions } from '../../../common/config/editorOptions.js'; -import { IEditorContribution } from '../../../common/editorCommon.js'; +import { IEditorContribution, IEditorDecorationsCollection } from '../../../common/editorCommon.js'; import { StandardTokenType } from '../../../common/encodedTokenAttributes.js'; import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js'; import { IModelDeltaDecoration, MinimapPosition, MinimapSectionHeaderStyle, TrackedRangeStickiness } from '../../../common/model.js'; @@ -21,7 +21,7 @@ export class SectionHeaderDetector extends Disposable implements IEditorContribu public static readonly ID: string = 'editor.sectionHeaderDetector'; private options: FindSectionHeaderOptions | undefined; - private decorations = this.editor.createDecorationsCollection(); + private decorations: IEditorDecorationsCollection; private computeSectionHeaders: RunOnceScheduler; private computePromise: CancelablePromise | null; private currentOccurrences: { [decorationId: string]: SectionHeaderOccurrence }; @@ -32,6 +32,7 @@ export class SectionHeaderDetector extends Disposable implements IEditorContribu @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, ) { super(); + this.decorations = this.editor.createDecorationsCollection(); this.options = this.createOptions(editor.getOption(EditorOption.minimap)); this.computePromise = null; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index fada2e0c7cf..c672613d27f 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -56,8 +56,10 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { private readonly _linesDomNodeScrollable: HTMLElement = document.createElement('div'); private readonly _linesDomNode: HTMLElement = document.createElement('div'); + private readonly _editor: ICodeEditor; + private _previousState: StickyScrollWidgetState | undefined; - private _lineHeight: number = this._editor.getOption(EditorOption.lineHeight); + private _lineHeight: number; private _renderedStickyLines: RenderedStickyLine[] = []; private _lineNumbers: number[] = []; private _lastLineRelativePosition: number = 0; @@ -71,10 +73,12 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { public readonly onDidChangeStickyScrollHeight = this._onDidChangeStickyScrollHeight.event; constructor( - private readonly _editor: ICodeEditor + editor: ICodeEditor ) { super(); + this._editor = editor; + this._lineHeight = editor.getOption(EditorOption.lineHeight); this._lineNumbersDomNode.className = 'sticky-widget-line-numbers'; this._lineNumbersDomNode.setAttribute('role', 'none'); @@ -85,7 +89,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { this._linesDomNodeScrollable.appendChild(this._linesDomNode); this._rootDomNode.className = 'sticky-widget'; - this._rootDomNode.classList.toggle('peek', _editor instanceof EmbeddedCodeEditorWidget); + this._rootDomNode.classList.toggle('peek', editor instanceof EmbeddedCodeEditorWidget); this._rootDomNode.appendChild(this._lineNumbersDomNode); this._rootDomNode.appendChild(this._linesDomNodeScrollable); this._setHeight(0); diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index 0ede4624f23..7f871915d76 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -517,8 +517,7 @@ export class SuggestController implements IEditorContribution { } private _reportSuggestionAcceptedTelemetry(item: CompletionItem, model: ITextModel, itemResolved: boolean, commandExectionDuration: number, additionalEditsAppliedAsync: number, index: number, completionItems: CompletionItem[]): void { - if (Math.floor(Math.random() * 100) === 0) { - // throttle telemetry event because accepting completions happens a lot + if (Math.random() > 0.0001) { // 0.01% return; } diff --git a/src/vs/editor/contrib/suggest/browser/suggestModel.ts b/src/vs/editor/contrib/suggest/browser/suggestModel.ts index 7a7019032ca..fcf97af35a6 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestModel.ts @@ -505,6 +505,7 @@ export class SuggestModel implements IDisposable { this._requestToken?.dispose(); if (!this._editor.hasModel()) { + completions.disposable.dispose(); return; } @@ -514,6 +515,7 @@ export class SuggestModel implements IDisposable { } if (this._triggerState === undefined) { + completions.disposable.dispose(); return; } @@ -561,11 +563,12 @@ export class SuggestModel implements IDisposable { }).catch(onUnexpectedError); } - private _telemetryGate: number = 0; - + /** + * Report durations telemetry with a 1% sampling rate. + * The telemetry is reported only if a random number between 0 and 100 is less than or equal to 1. + */ private _reportDurationsTelemetry(durations: CompletionDurations): void { - - if (this._telemetryGate++ % 230 !== 0) { + if (Math.random() > 0.0001) { // 0.01% return; } diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index fbaa5530d89..8c77c920b40 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -35,6 +35,7 @@ import { canExpandCompletionItem, SuggestDetailsOverlay, SuggestDetailsWidget } import { ItemRenderer } from './suggestWidgetRenderer.js'; import { getListStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { status } from '../../../../base/browser/ui/aria/aria.js'; +import { CompletionItemKinds } from '../../../common/languages.js'; /** * Suggest widget colors @@ -236,17 +237,19 @@ export class SuggestWidget implements IDisposable { getAriaLabel: (item: CompletionItem) => { let label = item.textLabel; + const kindLabel = CompletionItemKinds.toLabel(item.completion.kind); if (typeof item.completion.label !== 'string') { const { detail, description } = item.completion.label; if (detail && description) { - label = nls.localize('label.full', '{0} {1}, {2}', label, detail, description); + label = nls.localize('label.full', '{0} {1}, {2}, {3}', label, detail, description, kindLabel); } else if (detail) { - label = nls.localize('label.detail', '{0} {1}', label, detail); + label = nls.localize('label.detail', '{0} {1}, {2}', label, detail, kindLabel); } else if (description) { - label = nls.localize('label.desc', '{0}, {1}', label, description); + label = nls.localize('label.desc', '{0}, {1}, {2}', label, description, kindLabel); } + } else { + label = nls.localize('label', '{0}, {1}', label, kindLabel); } - if (!item.isResolved || !this._isDetailsVisible()) { return label; } @@ -1041,3 +1044,4 @@ export class SuggestContentWidget implements IContentWidget { this._position = position; } } + diff --git a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.css b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.css index a025a03ac2a..475006090b4 100644 --- a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.css +++ b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.css @@ -11,10 +11,6 @@ .monaco-workbench .codicon.codicon-symbol-class { color: var(--vscode-symbolIcon-classForeground); } .monaco-editor .codicon.codicon-symbol-method, .monaco-workbench .codicon.codicon-symbol-method { color: var(--vscode-symbolIcon-methodForeground); } -.monaco-editor .codicon.codicon-symbol-method-arrow, -.monaco-workbench .codicon.codicon-symbol-method-arrow { color: var(--vscode-symbolIcon-methodArrowForeground); } -.monaco-editor .codicon.codicon-flag, -.monaco-workbench .codicon.codicon-flag { color: var(--vscode-symbolIcon-flagForeground); } .monaco-editor .codicon.codicon-symbol-color, .monaco-workbench .codicon.codicon-symbol-color { color: var(--vscode-symbolIcon-colorForeground); } .monaco-editor .codicon.codicon-symbol-constant, diff --git a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts index 0cd56619647..a2795f7541c 100644 --- a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts +++ b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.ts @@ -36,9 +36,6 @@ export const SYMBOL_ICON_ENUMERATOR_FOREGROUND = registerColor('symbolIcon.enume hcLight: '#D67E00' }, localize('symbolIcon.enumeratorForeground', 'The foreground color for enumerator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - -export const SYMBOL_ICON_FLAG_FOREGROUND = registerColor('symbolIcon.flagForeground', SYMBOL_ICON_ENUMERATOR_FOREGROUND, localize('symbolIcon.enumeratorForeground', 'The foreground color for enumerator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); - export const SYMBOL_ICON_ENUMERATOR_MEMBER_FOREGROUND = registerColor('symbolIcon.enumeratorMemberForeground', { dark: '#75BEFF', light: '#007ACC', @@ -89,8 +86,6 @@ export const SYMBOL_ICON_METHOD_FOREGROUND = registerColor('symbolIcon.methodFor hcLight: '#652D90' }, localize('symbolIcon.methodForeground', 'The foreground color for method symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); -export const SYMBOL_ICON_METHOD_ARROW_FOREGROUND = registerColor('symbolIcon.methodArrowForeground', SYMBOL_ICON_METHOD_FOREGROUND, localize('symbolIcon.methodArrowForeground', 'The foreground color for method with an arrow symbols. These symbols appear in the suggest widget.')); - export const SYMBOL_ICON_MODULE_FOREGROUND = registerColor('symbolIcon.moduleForeground', foreground, localize('symbolIcon.moduleForeground', 'The foreground color for module symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); export const SYMBOL_ICON_NAMESPACE_FOREGROUND = registerColor('symbolIcon.namespaceForeground', foreground, localize('symbolIcon.namespaceForeground', 'The foreground color for namespace symbols. These symbols appear in the outline, breadcrumb, and suggest widget.')); diff --git a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts index 611fcb7e21c..2a65960d983 100644 --- a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts @@ -117,13 +117,15 @@ class Arrow { private static readonly _IdGenerator = new IdGenerator('.arrow-decoration-'); private readonly _ruleName = Arrow._IdGenerator.nextId(); - private readonly _decorations = this._editor.createDecorationsCollection(); + private readonly _decorations: IEditorDecorationsCollection; private _color: string | null = null; private _height: number = -1; constructor( private readonly _editor: ICodeEditor - ) { } + ) { + this._decorations = this._editor.createDecorationsCollection(); + } dispose(): void { this.hide(); diff --git a/src/vs/editor/test/common/codecs/markdownDecoder.test.ts b/src/vs/editor/test/common/codecs/markdownDecoder.test.ts index bff4b428ae1..d0ea73d7fa2 100644 --- a/src/vs/editor/test/common/codecs/markdownDecoder.test.ts +++ b/src/vs/editor/test/common/codecs/markdownDecoder.test.ts @@ -3,23 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; import { TestDecoder } from '../utils/testDecoder.js'; import { Range } from '../../../common/core/range.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { newWriteableStream } from '../../../../base/common/stream.js'; import { Tab } from '../../../common/codecs/simpleCodec/tokens/tab.js'; import { Word } from '../../../common/codecs/simpleCodec/tokens/word.js'; +import { Dash } from '../../../common/codecs/simpleCodec/tokens/dash.js'; import { Space } from '../../../common/codecs/simpleCodec/tokens/space.js'; import { NewLine } from '../../../common/codecs/linesCodec/tokens/newLine.js'; +import { FormFeed } from '../../../common/codecs/simpleCodec/tokens/formFeed.js'; import { VerticalTab } from '../../../common/codecs/simpleCodec/tokens/verticalTab.js'; import { MarkdownLink } from '../../../common/codecs/markdownCodec/tokens/markdownLink.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { MarkdownDecoder, TMarkdownToken } from '../../../common/codecs/markdownCodec/markdownDecoder.js'; -import { FormFeed } from '../../../common/codecs/simpleCodec/tokens/formFeed.js'; -import { LeftParenthesis, RightParenthesis } from '../../../common/codecs/simpleCodec/tokens/parentheses.js'; -import { LeftBracket, RightBracket } from '../../../common/codecs/simpleCodec/tokens/brackets.js'; import { CarriageReturn } from '../../../common/codecs/linesCodec/tokens/carriageReturn.js'; -import assert from 'assert'; +import { MarkdownImage } from '../../../common/codecs/markdownCodec/tokens/markdownImage.js'; +import { ExclamationMark } from '../../../common/codecs/simpleCodec/tokens/exclamationMark.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { MarkdownComment } from '../../../common/codecs/markdownCodec/tokens/markdownComment.js'; +import { LeftBracket, RightBracket } from '../../../common/codecs/simpleCodec/tokens/brackets.js'; +import { MarkdownDecoder, TMarkdownToken } from '../../../common/codecs/markdownCodec/markdownDecoder.js'; +import { LeftParenthesis, RightParenthesis } from '../../../common/codecs/simpleCodec/tokens/parentheses.js'; +import { LeftAngleBracket, RightAngleBracket } from '../../../common/codecs/simpleCodec/tokens/angleBrackets.js'; /** * A reusable test utility that asserts that a `TestMarkdownDecoder` instance @@ -55,278 +60,785 @@ export class TestMarkdownDecoder extends TestDecoder { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - test('produces expected tokens', async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); + suite('• general', () => { + test('• base cases', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); - await test.run( - ' hello world\nhow are\t you [caption text](./some/file/path/refer🎨nce.md)?\v\n\n[(example)](another/path/with[-and-]-chars/folder)\t \n\t[#file:something.txt](/absolute/path/to/something.txt)', - [ - // first line - new Space(new Range(1, 1, 1, 2)), - new Word(new Range(1, 2, 1, 7), 'hello'), - new Space(new Range(1, 7, 1, 8)), - new Word(new Range(1, 8, 1, 13), 'world'), - new NewLine(new Range(1, 13, 1, 14)), - // second line - new Word(new Range(2, 1, 2, 4), 'how'), - new Space(new Range(2, 4, 2, 5)), - new Word(new Range(2, 5, 2, 8), 'are'), - new Tab(new Range(2, 8, 2, 9)), - new Space(new Range(2, 9, 2, 10)), - new Word(new Range(2, 10, 2, 13), 'you'), - new Space(new Range(2, 13, 2, 14)), - new MarkdownLink(2, 14, '[caption text]', '(./some/file/path/refer🎨nce.md)'), - new Word(new Range(2, 60, 2, 61), '?'), - new VerticalTab(new Range(2, 61, 2, 62)), - new NewLine(new Range(2, 62, 2, 63)), - // third line - new NewLine(new Range(3, 1, 3, 2)), - // fourth line - new MarkdownLink(4, 1, '[(example)]', '(another/path/with[-and-]-chars/folder)'), - new Tab(new Range(4, 51, 4, 52)), - new Space(new Range(4, 52, 4, 53)), - new NewLine(new Range(4, 53, 4, 54)), - // fifth line - new Tab(new Range(5, 1, 5, 2)), - new MarkdownLink(5, 2, '[#file:something.txt]', '(/absolute/path/to/something.txt)'), - ], - ); - }); + await test.run( + [ + // basic text + ' hello world', + // text with markdown link and special characters in the filename + 'how are\t you [caption text](./some/file/path/refer🎨nce.md)?\v', + // empty line + '', + // markdown link with special characters in the link caption and path + '[(example!)](another/path/with[-and-]-chars/folder)\t ', + // markdown link `#file` variable in the caption and with absolute path + '\t[#file:something.txt](/absolute/path/to/something.txt)', + // text with a commented out markdown link + '\v\f machines must suffer', + ], + [ + // first line + new Space(new Range(1, 1, 1, 2)), + new Word(new Range(1, 2, 1, 7), 'hello'), + new Space(new Range(1, 7, 1, 8)), + new Word(new Range(1, 8, 1, 13), 'world'), + new NewLine(new Range(1, 13, 1, 14)), + // second line + new Word(new Range(2, 1, 2, 4), 'how'), + new Space(new Range(2, 4, 2, 5)), + new Word(new Range(2, 5, 2, 8), 'are'), + new Tab(new Range(2, 8, 2, 9)), + new Space(new Range(2, 9, 2, 10)), + new Word(new Range(2, 10, 2, 13), 'you'), + new Space(new Range(2, 13, 2, 14)), + new MarkdownLink(2, 14, '[caption text]', '(./some/file/path/refer🎨nce.md)'), + new Word(new Range(2, 60, 2, 61), '?'), + new VerticalTab(new Range(2, 61, 2, 62)), + new NewLine(new Range(2, 62, 2, 63)), + // third line + new NewLine(new Range(3, 1, 3, 2)), + // fourth line + new MarkdownLink(4, 1, '[(example!)]', '(another/path/with[-and-]-chars/folder)'), + new Tab(new Range(4, 52, 4, 53)), + new Space(new Range(4, 53, 4, 54)), + new NewLine(new Range(4, 54, 4, 55)), + // fifth line + new Tab(new Range(5, 1, 5, 2)), + new MarkdownLink(5, 2, '[#file:something.txt]', '(/absolute/path/to/something.txt)'), + new NewLine(new Range(5, 56, 5, 57)), + // sixth line + new VerticalTab(new Range(6, 1, 6, 2)), + new FormFeed(new Range(6, 2, 6, 3)), + new Space(new Range(6, 3, 6, 4)), + new Word(new Range(6, 4, 6, 12), 'machines'), + new Space(new Range(6, 12, 6, 13)), + new Word(new Range(6, 13, 6, 17), 'must'), + new Space(new Range(6, 17, 6, 18)), + new MarkdownComment(new Range(6, 18, 6, 18 + 41), ''), + new Space(new Range(6, 59, 6, 60)), + new Word(new Range(6, 60, 6, 66), 'suffer'), + ], + ); + }); - test('handles complex cases', async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - const inputLines = [ - // tests that the link caption contain a chat prompt `#file:` reference, while - // the file path can contain other `graphical characters` - '\v\t[#file:./another/path/to/file.txt](./real/filepath/file◆name.md)', - // tests that the link file path contain a chat prompt `#file:` reference, - // `spaces`, `emojies`, and other `graphical characters` - ' [reference ∘ label](/absolute/pa th/to-#file:file.txt/f🥸⚡️le.md)', - // tests that link caption and file path can contain `parentheses`, `spaces`, and - // `emojies` - '\f[!(hello)!](./w(())rld/nice-🦚-filen(a)me.git))\n\t', - // tests that the link caption can be empty, while the file path can contain `square brackets` - '[](./s[]me/pa[h!) ', - ]; - - await test.run( - inputLines, - [ - // `1st` line - new VerticalTab(new Range(1, 1, 1, 2)), - new Tab(new Range(1, 2, 1, 3)), - new MarkdownLink(1, 3, '[#file:./another/path/to/file.txt]', '(./real/filepath/file◆name.md)'), - new NewLine(new Range(1, 67, 1, 68)), - // `2nd` line - new Space(new Range(2, 1, 2, 2)), - new MarkdownLink(2, 2, '[reference ∘ label]', '(/absolute/pa th/to-#file:file.txt/f🥸⚡️le.md)'), - new NewLine(new Range(2, 67, 2, 68)), - // `3rd` line - new FormFeed(new Range(3, 1, 3, 2)), - new MarkdownLink(3, 2, '[!(hello)!]', '(./w(())rld/nice-🦚-filen(a)me.git)'), - new RightParenthesis(new Range(3, 48, 3, 49)), - new NewLine(new Range(3, 49, 3, 50)), - // `4th` line - new Tab(new Range(4, 1, 4, 2)), - new NewLine(new Range(4, 2, 4, 3)), - // `5th` line - new MarkdownLink(5, 1, '[]', '(./s[]me/pa[h!)'), - new Space(new Range(5, 18, 5, 19)), - ], - ); - }); - - suite('broken links', () => { - test('incomplete/invalid links', async () => { + test('• nuanced', async () => { const test = testDisposables.add( new TestMarkdownDecoder(), ); const inputLines = [ - // incomplete link reference with empty caption - '[ ](./real/file path/file⇧name.md', - // space between caption and reference is disallowed - '[link text] (./file path/name.txt)', + // tests that the link caption contain a chat prompt `#file:` reference, while + // the file path can contain other `graphical characters` + '\v\t[#file:./another/path/to/file.txt](./real/file!path/file◆name.md)', + // tests that the link file path contain a chat prompt `#file:` reference, + // `spaces`, `emojies`, and other `graphical characters` + ' [reference ∘ label](/absolute/pa th/to-#file:file.txt/f🥸⚡️le.md)', + // tests that link caption and file path can contain `parentheses`, `spaces`, and + // `emojies` + '\f[!(hello)!](./w(())rld/nice-🦚-filen(a).git))\n\t', + // tests that the link caption can be empty, while the file path can contain `square brackets` + '[](./s[]me/pa[h!) ', ]; await test.run( inputLines, [ // `1st` line - new LeftBracket(new Range(1, 1, 1, 2)), - new Space(new Range(1, 2, 1, 3)), - new RightBracket(new Range(1, 3, 1, 4)), - new LeftParenthesis(new Range(1, 4, 1, 5)), - new Word(new Range(1, 5, 1, 5 + 11), './real/file'), - new Space(new Range(1, 16, 1, 17)), - new Word(new Range(1, 17, 1, 17 + 17), 'path/file⇧name.md'), - new NewLine(new Range(1, 34, 1, 35)), + new VerticalTab(new Range(1, 1, 1, 2)), + new Tab(new Range(1, 2, 1, 3)), + new MarkdownLink(1, 3, '[#file:./another/path/to/file.txt]', '(./real/file!path/file◆name.md)'), + new NewLine(new Range(1, 68, 1, 69)), // `2nd` line - new LeftBracket(new Range(2, 1, 2, 2)), - new Word(new Range(2, 2, 2, 2 + 4), 'link'), - new Space(new Range(2, 6, 2, 7)), - new Word(new Range(2, 7, 2, 7 + 4), 'text'), - new RightBracket(new Range(2, 11, 2, 12)), - new Space(new Range(2, 12, 2, 13)), - new LeftParenthesis(new Range(2, 13, 2, 14)), - new Word(new Range(2, 14, 2, 14 + 6), './file'), - new Space(new Range(2, 20, 2, 21)), - new Word(new Range(2, 21, 2, 21 + 13), 'path/name.txt'), - new RightParenthesis(new Range(2, 34, 2, 35)), + new Space(new Range(2, 1, 2, 2)), + new MarkdownLink(2, 2, '[reference ∘ label]', '(/absolute/pa th/to-#file:file.txt/f🥸⚡️le.md)'), + new NewLine(new Range(2, 67, 2, 68)), + // `3rd` line + new FormFeed(new Range(3, 1, 3, 2)), + new MarkdownLink(3, 2, '[!(hello)!]', '(./w(())rld/nice-🦚-filen(a).git)'), + new RightParenthesis(new Range(3, 50, 3, 51)), + new NewLine(new Range(3, 51, 3, 52)), + // `4th` line + new Tab(new Range(4, 1, 4, 2)), + new NewLine(new Range(4, 2, 4, 3)), + // `5th` line + new MarkdownLink(5, 1, '[]', '(./s[]me/pa[h!)'), + new Space(new Range(5, 24, 5, 25)), ], ); }); + }); - suite('stop characters inside caption/reference (new lines)', () => { - for (const stopCharacter of [CarriageReturn, NewLine]) { - let characterName = ''; - - if (stopCharacter === CarriageReturn) { - characterName = '\\r'; - } - if (stopCharacter === NewLine) { - characterName = '\\n'; - } - - assert( - characterName !== '', - 'The "characterName" must be set, got "empty line".', + suite('• links', () => { + suite('• broken', () => { + test('• invalid', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), ); - test(`stop character - "${characterName}"`, async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), + const inputLines = [ + // incomplete link reference with empty caption + '[ ](./real/file path/file⇧name.md', + // space between caption and reference is disallowed + '[link text] (./file path/name.txt)', + ]; + + await test.run( + inputLines, + [ + // `1st` line + new LeftBracket(new Range(1, 1, 1, 2)), + new Space(new Range(1, 2, 1, 3)), + new RightBracket(new Range(1, 3, 1, 4)), + new LeftParenthesis(new Range(1, 4, 1, 5)), + new Word(new Range(1, 5, 1, 5 + 11), './real/file'), + new Space(new Range(1, 16, 1, 17)), + new Word(new Range(1, 17, 1, 17 + 17), 'path/file⇧name.md'), + new NewLine(new Range(1, 34, 1, 35)), + // `2nd` line + new LeftBracket(new Range(2, 1, 2, 2)), + new Word(new Range(2, 2, 2, 2 + 4), 'link'), + new Space(new Range(2, 6, 2, 7)), + new Word(new Range(2, 7, 2, 7 + 4), 'text'), + new RightBracket(new Range(2, 11, 2, 12)), + new Space(new Range(2, 12, 2, 13)), + new LeftParenthesis(new Range(2, 13, 2, 14)), + new Word(new Range(2, 14, 2, 14 + 6), './file'), + new Space(new Range(2, 20, 2, 21)), + new Word(new Range(2, 21, 2, 21 + 13), 'path/name.txt'), + new RightParenthesis(new Range(2, 34, 2, 35)), + ], + ); + }); + + suite('• stop characters inside caption/reference (new lines)', () => { + for (const stopCharacter of [CarriageReturn, NewLine]) { + let characterName = ''; + + if (stopCharacter === CarriageReturn) { + characterName = '\\r'; + } + if (stopCharacter === NewLine) { + characterName = '\\n'; + } + + assert( + characterName !== '', + 'The "characterName" must be set, got "empty line".', ); - const inputLines = [ - // stop character inside link caption - `[haa${stopCharacter.symbol}loů](./real/💁/name.txt)`, - // stop character inside link reference - `[ref text](/etc/pat${stopCharacter.symbol}h/to/file.md)`, - // stop character between line caption and link reference is disallowed - `[text]${stopCharacter.symbol}(/etc/ path/file.md)`, - ]; + test(`• stop character - "${characterName}"`, async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // stop character inside link caption + `[haa${stopCharacter.symbol}loů](./real/💁/name.txt)`, + // stop character inside link reference + `[ref text](/etc/pat${stopCharacter.symbol}h/to/file.md)`, + // stop character between line caption and link reference is disallowed + `[text]${stopCharacter.symbol}(/etc/ path/file.md)`, + ]; - await test.run( - inputLines, - [ - // `1st` input line - new LeftBracket(new Range(1, 1, 1, 2)), - new Word(new Range(1, 2, 1, 2 + 3), 'haa'), - new stopCharacter(new Range(1, 5, 1, 6)), // <- stop character - new Word(new Range(2, 1, 2, 1 + 3), 'loů'), - new RightBracket(new Range(2, 4, 2, 5)), - new LeftParenthesis(new Range(2, 5, 2, 6)), - new Word(new Range(2, 6, 2, 6 + 18), './real/💁/name.txt'), - new RightParenthesis(new Range(2, 24, 2, 25)), - new NewLine(new Range(2, 25, 2, 26)), - // `2nd` input line - new LeftBracket(new Range(3, 1, 3, 2)), - new Word(new Range(3, 2, 3, 2 + 3), 'ref'), - new Space(new Range(3, 5, 3, 6)), - new Word(new Range(3, 6, 3, 6 + 4), 'text'), - new RightBracket(new Range(3, 10, 3, 11)), - new LeftParenthesis(new Range(3, 11, 3, 12)), - new Word(new Range(3, 12, 3, 12 + 8), '/etc/pat'), - new stopCharacter(new Range(3, 20, 3, 21)), // <- stop character - new Word(new Range(4, 1, 4, 1 + 12), 'h/to/file.md'), - new RightParenthesis(new Range(4, 13, 4, 14)), - new NewLine(new Range(4, 14, 4, 15)), - // `3nd` input line - new LeftBracket(new Range(5, 1, 5, 2)), - new Word(new Range(5, 2, 5, 2 + 4), 'text'), - new RightBracket(new Range(5, 6, 5, 7)), - new stopCharacter(new Range(5, 7, 5, 8)), // <- stop character - new LeftParenthesis(new Range(6, 1, 6, 2)), - new Word(new Range(6, 2, 6, 2 + 5), '/etc/'), - new Space(new Range(6, 7, 6, 8)), - new Word(new Range(6, 8, 6, 8 + 12), 'path/file.md'), - new RightParenthesis(new Range(6, 20, 6, 21)), - ], + await test.run( + inputLines, + [ + // `1st` input line + new LeftBracket(new Range(1, 1, 1, 2)), + new Word(new Range(1, 2, 1, 2 + 3), 'haa'), + new stopCharacter(new Range(1, 5, 1, 6)), // <- stop character + new Word(new Range(2, 1, 2, 1 + 3), 'loů'), + new RightBracket(new Range(2, 4, 2, 5)), + new LeftParenthesis(new Range(2, 5, 2, 6)), + new Word(new Range(2, 6, 2, 6 + 18), './real/💁/name.txt'), + new RightParenthesis(new Range(2, 24, 2, 25)), + new NewLine(new Range(2, 25, 2, 26)), + // `2nd` input line + new LeftBracket(new Range(3, 1, 3, 2)), + new Word(new Range(3, 2, 3, 2 + 3), 'ref'), + new Space(new Range(3, 5, 3, 6)), + new Word(new Range(3, 6, 3, 6 + 4), 'text'), + new RightBracket(new Range(3, 10, 3, 11)), + new LeftParenthesis(new Range(3, 11, 3, 12)), + new Word(new Range(3, 12, 3, 12 + 8), '/etc/pat'), + new stopCharacter(new Range(3, 20, 3, 21)), // <- stop character + new Word(new Range(4, 1, 4, 1 + 12), 'h/to/file.md'), + new RightParenthesis(new Range(4, 13, 4, 14)), + new NewLine(new Range(4, 14, 4, 15)), + // `3nd` input line + new LeftBracket(new Range(5, 1, 5, 2)), + new Word(new Range(5, 2, 5, 2 + 4), 'text'), + new RightBracket(new Range(5, 6, 5, 7)), + new stopCharacter(new Range(5, 7, 5, 8)), // <- stop character + new LeftParenthesis(new Range(6, 1, 6, 2)), + new Word(new Range(6, 2, 6, 2 + 5), '/etc/'), + new Space(new Range(6, 7, 6, 8)), + new Word(new Range(6, 8, 6, 8 + 12), 'path/file.md'), + new RightParenthesis(new Range(6, 20, 6, 21)), + ], + ); + }); + } + }); + + /** + * Same as above but these stop characters do not move the caret to the next line. + */ + suite('• stop characters inside caption/reference (same line)', () => { + for (const stopCharacter of [VerticalTab, FormFeed]) { + let characterName = ''; + + if (stopCharacter === VerticalTab) { + characterName = '\\v'; + } + if (stopCharacter === FormFeed) { + characterName = '\\f'; + } + + assert( + characterName !== '', + 'The "characterName" must be set, got "empty line".', ); - }); - } + + test(`• stop character - "${characterName}"`, async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // stop character inside link caption + `[haa${stopCharacter.symbol}loů](./real/💁/name.txt)`, + // stop character inside link reference + `[ref text](/etc/pat${stopCharacter.symbol}h/to/file.md)`, + // stop character between line caption and link reference is disallowed + `[text]${stopCharacter.symbol}(/etc/ path/file.md)`, + ]; + + + await test.run( + inputLines, + [ + // `1st` input line + new LeftBracket(new Range(1, 1, 1, 2)), + new Word(new Range(1, 2, 1, 2 + 3), 'haa'), + new stopCharacter(new Range(1, 5, 1, 6)), // <- stop character + new Word(new Range(1, 6, 1, 6 + 3), 'loů'), + new RightBracket(new Range(1, 9, 1, 10)), + new LeftParenthesis(new Range(1, 10, 1, 11)), + new Word(new Range(1, 11, 1, 11 + 18), './real/💁/name.txt'), + new RightParenthesis(new Range(1, 29, 1, 30)), + new NewLine(new Range(1, 30, 1, 31)), + // `2nd` input line + new LeftBracket(new Range(2, 1, 2, 2)), + new Word(new Range(2, 2, 2, 2 + 3), 'ref'), + new Space(new Range(2, 5, 2, 6)), + new Word(new Range(2, 6, 2, 6 + 4), 'text'), + new RightBracket(new Range(2, 10, 2, 11)), + new LeftParenthesis(new Range(2, 11, 2, 12)), + new Word(new Range(2, 12, 2, 12 + 8), '/etc/pat'), + new stopCharacter(new Range(2, 20, 2, 21)), // <- stop character + new Word(new Range(2, 21, 2, 21 + 12), 'h/to/file.md'), + new RightParenthesis(new Range(2, 33, 2, 34)), + new NewLine(new Range(2, 34, 2, 35)), + // `3nd` input line + new LeftBracket(new Range(3, 1, 3, 2)), + new Word(new Range(3, 2, 3, 2 + 4), 'text'), + new RightBracket(new Range(3, 6, 3, 7)), + new stopCharacter(new Range(3, 7, 3, 8)), // <- stop character + new LeftParenthesis(new Range(3, 8, 3, 9)), + new Word(new Range(3, 9, 3, 9 + 5), '/etc/'), + new Space(new Range(3, 14, 3, 15)), + new Word(new Range(3, 15, 3, 15 + 12), 'path/file.md'), + new RightParenthesis(new Range(3, 27, 3, 28)), + ], + ); + }); + } + }); + }); + }); + + + suite('• images', () => { + suite('• general', () => { + test('• base cases', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputData = [ + '\t![alt text](./some/path/to/file.jpg) ', + 'plain text \f![label](./image.png)\v and more text', + '![](/var/images/default) following text', + ]; + + await test.run( + inputData, + [ + // `1st` + new Tab(new Range(1, 1, 1, 2)), + new MarkdownImage(1, 2, '![alt text]', '(./some/path/to/file.jpg)'), + new Space(new Range(1, 38, 1, 39)), + new NewLine(new Range(1, 39, 1, 40)), + // `2nd` + new Word(new Range(2, 1, 2, 6), 'plain'), + new Space(new Range(2, 6, 2, 7)), + new Word(new Range(2, 7, 2, 11), 'text'), + new Space(new Range(2, 11, 2, 12)), + new FormFeed(new Range(2, 12, 2, 13)), + new MarkdownImage(2, 13, '![label]', '(./image.png)'), + new VerticalTab(new Range(2, 34, 2, 35)), + new Space(new Range(2, 35, 2, 36)), + new Word(new Range(2, 36, 2, 39), 'and'), + new Space(new Range(2, 39, 2, 40)), + new Word(new Range(2, 40, 2, 44), 'more'), + new Space(new Range(2, 44, 2, 45)), + new Word(new Range(2, 45, 2, 49), 'text'), + new NewLine(new Range(2, 49, 2, 50)), + // `3rd` + new MarkdownImage(3, 1, '![]', '(/var/images/default)'), + new Space(new Range(3, 25, 3, 26)), + new Word(new Range(3, 26, 3, 35), 'following'), + new Space(new Range(3, 35, 3, 36)), + new Word(new Range(3, 36, 3, 40), 'text'), + ], + ); + }); + + test('• nuanced', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputData = [ + '\t![](./s☻me/path/to/file.jpeg) ', + 'raw text \f![(/1.png)](./image-🥸.png)\v and more text', + // '![](/var/images/default) following text', + ]; + + await test.run( + inputData, + [ + // `1st` + new Tab(new Range(1, 1, 1, 2)), + new MarkdownImage(1, 2, '![]', '(./s☻me/path/to/file.jpeg)'), + new Space(new Range(1, 47, 1, 48)), + new NewLine(new Range(1, 48, 1, 49)), + // `2nd` + new Word(new Range(2, 1, 2, 4), 'raw'), + new Space(new Range(2, 4, 2, 5)), + new Word(new Range(2, 5, 2, 9), 'text'), + new Space(new Range(2, 9, 2, 10)), + new FormFeed(new Range(2, 10, 2, 11)), + new MarkdownImage(2, 11, '![(/1.png)]', '(./image-🥸.png)'), + new VerticalTab(new Range(2, 38, 2, 39)), + new Space(new Range(2, 39, 2, 40)), + new Word(new Range(2, 40, 2, 43), 'and'), + new Space(new Range(2, 43, 2, 44)), + new Word(new Range(2, 44, 2, 48), 'more'), + new Space(new Range(2, 48, 2, 49)), + new Word(new Range(2, 49, 2, 53), 'text'), + ], + ); + }); }); - /** - * Same as above but these stop characters do not move the caret to the next line. - */ - suite('stop characters inside caption/reference (same line)', () => { - for (const stopCharacter of [VerticalTab, FormFeed]) { - let characterName = ''; - - if (stopCharacter === VerticalTab) { - characterName = '\\v'; - } - if (stopCharacter === FormFeed) { - characterName = '\\f'; - } - - assert( - characterName !== '', - 'The "characterName" must be set, got "empty line".', + suite('• broken', () => { + test('• invalid', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), ); - test(`stop character - "${characterName}"`, async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), + const inputLines = [ + // incomplete link reference with empty caption + '![ ](./real/file path/file★name.webp', + // space between caption and reference is disallowed + '\f![link text] (./file path/name.jpg)', + // new line inside the link reference + '\v![ ](./file\npath/name.jpg )', + ]; + + await test.run( + inputLines, + [ + // `1st` line + new ExclamationMark(new Range(1, 1, 1, 2)), + new LeftBracket(new Range(1, 2, 1, 3)), + new Space(new Range(1, 3, 1, 4)), + new RightBracket(new Range(1, 4, 1, 5)), + new LeftParenthesis(new Range(1, 5, 1, 6)), + new Word(new Range(1, 6, 1, 6 + 11), './real/file'), + new Space(new Range(1, 17, 1, 18)), + new Word(new Range(1, 18, 1, 18 + 19), 'path/file★name.webp'), + new NewLine(new Range(1, 37, 1, 38)), + // `2nd` line + new FormFeed(new Range(2, 1, 2, 2)), + new ExclamationMark(new Range(2, 2, 2, 3)), + new LeftBracket(new Range(2, 3, 2, 4)), + new Word(new Range(2, 4, 2, 4 + 4), 'link'), + new Space(new Range(2, 8, 2, 9)), + new Word(new Range(2, 9, 2, 9 + 4), 'text'), + new RightBracket(new Range(2, 13, 2, 14)), + new Space(new Range(2, 14, 2, 15)), + new LeftParenthesis(new Range(2, 15, 2, 16)), + new Word(new Range(2, 16, 2, 16 + 6), './file'), + new Space(new Range(2, 22, 2, 23)), + new Word(new Range(2, 23, 2, 23 + 13), 'path/name.jpg'), + new RightParenthesis(new Range(2, 36, 2, 37)), + new NewLine(new Range(2, 37, 2, 38)), + // `3rd` line + new VerticalTab(new Range(3, 1, 3, 2)), + new ExclamationMark(new Range(3, 2, 3, 3)), + new LeftBracket(new Range(3, 3, 3, 4)), + new Space(new Range(3, 4, 3, 5)), + new RightBracket(new Range(3, 5, 3, 6)), + new LeftParenthesis(new Range(3, 6, 3, 7)), + new Word(new Range(3, 7, 3, 7 + 6), './file'), + new NewLine(new Range(3, 13, 3, 14)), + new Word(new Range(4, 1, 4, 1 + 13), 'path/name.jpg'), + new Space(new Range(4, 14, 4, 15)), + new RightParenthesis(new Range(4, 15, 4, 16)), + ], + ); + }); + + suite('• stop characters inside caption/reference (new lines)', () => { + for (const stopCharacter of [CarriageReturn, NewLine]) { + let characterName = ''; + + if (stopCharacter === CarriageReturn) { + characterName = '\\r'; + } + if (stopCharacter === NewLine) { + characterName = '\\n'; + } + + assert( + characterName !== '', + 'The "characterName" must be set, got "empty line".', ); - const inputLines = [ - // stop character inside link caption - `[haa${stopCharacter.symbol}loů](./real/💁/name.txt)`, - // stop character inside link reference - `[ref text](/etc/pat${stopCharacter.symbol}h/to/file.md)`, - // stop character between line caption and link reference is disallowed - `[text]${stopCharacter.symbol}(/etc/ path/file.md)`, - ]; + test(`• stop character - "${characterName}"`, async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // stop character inside link caption + `![haa${stopCharacter.symbol}loů](./real/💁/name.png)`, + // stop character inside link reference + `![ref text](/etc/pat${stopCharacter.symbol}h/to/file.webp)`, + // stop character between line caption and link reference is disallowed + `![text]${stopCharacter.symbol}(/etc/ path/file.jpeg)`, + ]; - await test.run( - inputLines, - [ - // `1st` input line - new LeftBracket(new Range(1, 1, 1, 2)), - new Word(new Range(1, 2, 1, 2 + 3), 'haa'), - new stopCharacter(new Range(1, 5, 1, 6)), // <- stop character - new Word(new Range(1, 6, 1, 6 + 3), 'loů'), - new RightBracket(new Range(1, 9, 1, 10)), - new LeftParenthesis(new Range(1, 10, 1, 11)), - new Word(new Range(1, 11, 1, 11 + 18), './real/💁/name.txt'), - new RightParenthesis(new Range(1, 29, 1, 30)), - new NewLine(new Range(1, 30, 1, 31)), - // `2nd` input line - new LeftBracket(new Range(2, 1, 2, 2)), - new Word(new Range(2, 2, 2, 2 + 3), 'ref'), - new Space(new Range(2, 5, 2, 6)), - new Word(new Range(2, 6, 2, 6 + 4), 'text'), - new RightBracket(new Range(2, 10, 2, 11)), - new LeftParenthesis(new Range(2, 11, 2, 12)), - new Word(new Range(2, 12, 2, 12 + 8), '/etc/pat'), - new stopCharacter(new Range(2, 20, 2, 21)), // <- stop character - new Word(new Range(2, 21, 2, 21 + 12), 'h/to/file.md'), - new RightParenthesis(new Range(2, 33, 2, 34)), - new NewLine(new Range(2, 34, 2, 35)), - // `3nd` input line - new LeftBracket(new Range(3, 1, 3, 2)), - new Word(new Range(3, 2, 3, 2 + 4), 'text'), - new RightBracket(new Range(3, 6, 3, 7)), - new stopCharacter(new Range(3, 7, 3, 8)), // <- stop character - new LeftParenthesis(new Range(3, 8, 3, 9)), - new Word(new Range(3, 9, 3, 9 + 5), '/etc/'), - new Space(new Range(3, 14, 3, 15)), - new Word(new Range(3, 15, 3, 15 + 12), 'path/file.md'), - new RightParenthesis(new Range(3, 27, 3, 28)), - ], + await test.run( + inputLines, + [ + // `1st` input line + new ExclamationMark(new Range(1, 1, 1, 2)), + new LeftBracket(new Range(1, 2, 1, 3)), + new Word(new Range(1, 3, 1, 3 + 3), 'haa'), + new stopCharacter(new Range(1, 6, 1, 7)), // <- stop character + new Word(new Range(2, 1, 2, 1 + 3), 'loů'), + new RightBracket(new Range(2, 4, 2, 5)), + new LeftParenthesis(new Range(2, 5, 2, 6)), + new Word(new Range(2, 6, 2, 6 + 18), './real/💁/name.png'), + new RightParenthesis(new Range(2, 24, 2, 25)), + new NewLine(new Range(2, 25, 2, 26)), + // `2nd` input line + new ExclamationMark(new Range(3, 1, 3, 2)), + new LeftBracket(new Range(3, 2, 3, 3)), + new Word(new Range(3, 3, 3, 3 + 3), 'ref'), + new Space(new Range(3, 6, 3, 7)), + new Word(new Range(3, 7, 3, 7 + 4), 'text'), + new RightBracket(new Range(3, 11, 3, 12)), + new LeftParenthesis(new Range(3, 12, 3, 13)), + new Word(new Range(3, 13, 3, 13 + 8), '/etc/pat'), + new stopCharacter(new Range(3, 21, 3, 22)), // <- stop character + new Word(new Range(4, 1, 4, 1 + 14), 'h/to/file.webp'), + new RightParenthesis(new Range(4, 15, 4, 16)), + new NewLine(new Range(4, 16, 4, 17)), + // `3nd` input line + new ExclamationMark(new Range(5, 1, 5, 2)), + new LeftBracket(new Range(5, 2, 5, 3)), + new Word(new Range(5, 3, 5, 3 + 4), 'text'), + new RightBracket(new Range(5, 7, 5, 8)), + new stopCharacter(new Range(5, 8, 5, 9)), // <- stop character + new LeftParenthesis(new Range(6, 1, 6, 2)), + new Word(new Range(6, 2, 6, 2 + 5), '/etc/'), + new Space(new Range(6, 7, 6, 8)), + new Word(new Range(6, 8, 6, 8 + 14), 'path/file.jpeg'), + new RightParenthesis(new Range(6, 22, 6, 23)), + ], + ); + }); + } + }); + + /** + * Same as above but these stop characters do not move the caret to the next line. + */ + suite('• stop characters inside caption/reference (same line)', () => { + for (const stopCharacter of [VerticalTab, FormFeed]) { + let characterName = ''; + + if (stopCharacter === VerticalTab) { + characterName = '\\v'; + } + if (stopCharacter === FormFeed) { + characterName = '\\f'; + } + + assert( + characterName !== '', + 'The "characterName" must be set, got "empty line".', ); - }); - } + + test(`• stop character - "${characterName}"`, async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputLines = [ + // stop character inside link caption + `![haa${stopCharacter.symbol}loů](./real/💁/name)`, + // stop character inside link reference + `![ref text](/etc/pat${stopCharacter.symbol}h/to/file.webp)`, + // stop character between line caption and link reference is disallowed + `![text]${stopCharacter.symbol}(/etc/ path/image.gif)`, + ]; + + + await test.run( + inputLines, + [ + // `1st` input line + new ExclamationMark(new Range(1, 1, 1, 2)), + new LeftBracket(new Range(1, 2, 1, 3)), + new Word(new Range(1, 3, 1, 3 + 3), 'haa'), + new stopCharacter(new Range(1, 6, 1, 7)), // <- stop character + new Word(new Range(1, 7, 1, 7 + 3), 'loů'), + new RightBracket(new Range(1, 10, 1, 11)), + new LeftParenthesis(new Range(1, 11, 1, 12)), + new Word(new Range(1, 12, 1, 12 + 14), './real/💁/name'), + new RightParenthesis(new Range(1, 26, 1, 27)), + new NewLine(new Range(1, 27, 1, 28)), + // `2nd` input line + new ExclamationMark(new Range(2, 1, 2, 2)), + new LeftBracket(new Range(2, 2, 2, 3)), + new Word(new Range(2, 3, 2, 3 + 3), 'ref'), + new Space(new Range(2, 6, 2, 7)), + new Word(new Range(2, 7, 2, 7 + 4), 'text'), + new RightBracket(new Range(2, 11, 2, 12)), + new LeftParenthesis(new Range(2, 12, 2, 13)), + new Word(new Range(2, 13, 2, 13 + 8), '/etc/pat'), + new stopCharacter(new Range(2, 21, 2, 22)), // <- stop character + new Word(new Range(2, 22, 2, 22 + 14), 'h/to/file.webp'), + new RightParenthesis(new Range(2, 36, 2, 37)), + new NewLine(new Range(2, 37, 2, 38)), + // `3nd` input line + new ExclamationMark(new Range(3, 1, 3, 2)), + new LeftBracket(new Range(3, 2, 3, 3)), + new Word(new Range(3, 3, 3, 3 + 4), 'text'), + new RightBracket(new Range(3, 7, 3, 8)), + new stopCharacter(new Range(3, 8, 3, 9)), // <- stop character + new LeftParenthesis(new Range(3, 9, 3, 10)), + new Word(new Range(3, 10, 3, 10 + 5), '/etc/'), + new Space(new Range(3, 15, 3, 16)), + new Word(new Range(3, 16, 3, 16 + 14), 'path/image.gif'), + new RightParenthesis(new Range(3, 30, 3, 31)), + ], + ); + }); + } + }); + }); + }); + + suite('• comments', () => { + suite('• general', () => { + test('• base cases', async () => { + const test = testDisposables.add( + new TestMarkdownDecoder(), + ); + + const inputData = [ + // comment with text inside it + '\t', + // comment with a link inside + 'some text and more text ', + // comment new lines inside it + ' usual text follows', + // an empty comment + '\t\t', + // comment that was not closed properly + 'haalo\t'), + new NewLine(new Range(1, 22, 1, 23)), + // `2nd` + new Word(new Range(2, 1, 2, 5), 'some'), + new Space(new Range(2, 5, 2, 6)), + new Word(new Range(2, 6, 2, 10), 'text'), + new MarkdownComment(new Range(2, 10, 2, 10 + 46), ''), + new Space(new Range(2, 56, 2, 57)), + new Word(new Range(2, 57, 2, 60), 'and'), + new Space(new Range(2, 60, 2, 61)), + new Word(new Range(2, 61, 2, 65), 'more'), + new Space(new Range(2, 65, 2, 66)), + new Word(new Range(2, 66, 2, 70), 'text'), + new Space(new Range(2, 70, 2, 71)), + new NewLine(new Range(2, 71, 2, 72)), + // `3rd` + new MarkdownComment(new Range(3, 1, 3 + 3, 1 + 13), ''), + new Space(new Range(6, 14, 6, 15)), + new Word(new Range(6, 15, 6, 15 + 5), 'usual'), + new Space(new Range(6, 20, 6, 21)), + new Word(new Range(6, 21, 6, 21 + 4), 'text'), + new Space(new Range(6, 25, 6, 26)), + new Word(new Range(6, 26, 6, 26 + 7), 'follows'), + new NewLine(new Range(6, 33, 6, 34)), + // `4rd` + new Tab(new Range(7, 1, 7, 2)), + new MarkdownComment(new Range(7, 2, 7, 2 + 7), ''), + new Tab(new Range(7, 9, 7, 10)), + new NewLine(new Range(7, 10, 7, 11)), + // `5th` + new Word(new Range(8, 1, 8, 6), 'haalo'), + new Tab(new Range(8, 6, 8, 7)), + new MarkdownComment(new Range(8, 7, 8, 7 + 40), '>', + // comment contains `<[]>` brackets and `!` + '\t\t', + // comment contains `\t\t', + // comment contains `'), + new RightAngleBracket(new Range(1, 19, 1, 20)), + new NewLine(new Range(1, 20, 1, 21)), + // `2nd` + new MarkdownComment(new Range(2, 1, 2, 1 + 21), ''), + new Tab(new Range(2, 22, 2, 23)), + new Tab(new Range(2, 23, 2, 24)), + new NewLine(new Range(2, 24, 2, 25)), + // `3rd` + new VerticalTab(new Range(3, 1, 3, 2)), + new MarkdownComment(new Range(3, 2, 3 + 3, 1 + 7), ''), + new Tab(new Range(6, 8, 6, 9)), + new Tab(new Range(6, 9, 6, 10)), + new NewLine(new Range(6, 10, 6, 11)), + // `4rd` + new Space(new Range(7, 1, 7, 2)), + // note! comment does not have correct closing `-->`, hence the comment extends + // to the end of the text, and therefore includes the \t\v\f and space at the end + new MarkdownComment(new Range(7, 2, 8, 1 + 12), ' ', + ' < !-- світ -->\t', + '\v\f', + '`, hence the comment extends + // to the end of the text, and therefore includes the `space` at the end + new MarkdownComment(new Range(4, 1, 4, 1 + 15), ' s0 - // | | - // | | - // | e_user_r | e_user - // | | - // | | - // v e_ai_r v - /// d1 ---------------> s1 - // - // d0 - document snapshot - // s0 - document - // e_ai - ai edits - // e_user - user edits - // - const e_ai = this._edit; - const e_user = edit; - - const e_user_r = e_user.tryRebase(e_ai.inverse(this.originalModel.getValue()), true); - - if (e_user_r === undefined) { - // user edits overlaps/conflicts with AI edits - this._edit = e_ai.compose(e_user); - } else { - const edits = OffsetEdits.asEditOperations(e_user_r, this.originalModel); - this.originalModel.applyEdits(edits); - this._edit = e_ai.tryRebase(e_user_r); - } - - this._allEditsAreFromUs = false; - this._updateDiffInfoSeq(); - } - } - - acceptAgentEdits(textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void { - - // push stack element for the first edit - if (this._isFirstEditAfterStartOrSnapshot) { - this._isFirstEditAfterStartOrSnapshot = false; - const request = this._chatService.getSession(this._telemetryInfo.sessionId)?.getRequests().at(-1); - const label = request?.message.text ? localize('chatEditing1', "Chat Edit: '{0}'", request.message.text) : localize('chatEditing2', "Chat Edit"); - this._undoRedoService.pushElement(new SingleModelEditStackElement(label, 'chat.edit', this.modifiedModel, null)); - } - - const ops = textEdits.map(TextEdit.asEditOperation); - const undoEdits = this._applyEdits(ops); - - const maxLineNumber = undoEdits.reduce((max, op) => Math.max(max, op.range.startLineNumber), 0); - - const newDecorations: IModelDeltaDecoration[] = [ - // decorate pending edit (region) - { - options: ChatEditingNotebookCellEntry._pendingEditDecorationOptions, - range: new Range(maxLineNumber + 1, 1, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER) - } - ]; - - if (maxLineNumber > 0) { - // decorate last edit - newDecorations.push({ - options: ChatEditingNotebookCellEntry._lastEditDecorationOptions, - range: new Range(maxLineNumber, 1, maxLineNumber, Number.MAX_SAFE_INTEGER) - }); - } - - this._editDecorations = this.modifiedModel.deltaDecorations(this._editDecorations, newDecorations); - - - transaction((tx) => { - if (!isLastEdits) { - this._stateObs.set(WorkingSetEntryState.Modified, tx); - this._isCurrentlyBeingModifiedByObs.set(responseModel, tx); - this._maxModifiedLineNumber.set(maxLineNumber, tx); - - } else { - this._resetEditsState(tx); - this._updateDiffInfoSeq(); - this._maxModifiedLineNumber.set(0, tx); - this._editDecorationClear.schedule(); - } - }); - } - - scheduleEditDecorations() { - this._editDecorationClear.schedule(); - } - - protected _resetEditsState(tx: ITransaction): void { - this._isCurrentlyBeingModifiedByObs.set(undefined, tx); - this._maxModifiedLineNumber.set(0, tx); - } - - private async _acceptHunk(change: DetailedLineRangeMapping): Promise { - this._isEditFromUs = true; - try { - if (!this._diffInfo.get().changes.includes(change)) { - // diffInfo should have model version ids and check them (instead of the caller doing that) - return false; - } - const edits: ISingleEditOperation[] = []; - for (const edit of change.innerChanges ?? []) { - const newText = this.modifiedModel.getValueInRange(edit.modifiedRange); - edits.push(EditOperation.replace(edit.originalRange, newText)); - } - this.originalModel.pushEditOperations(null, edits, _ => null); - } - finally { - this._isEditFromUs = false; - } - await this._updateDiffInfoSeq(); - if (this._diffInfo.get().identical) { - this._stateObs.set(WorkingSetEntryState.Accepted, undefined); - } - return true; - } - - private async _rejectHunk(change: DetailedLineRangeMapping): Promise { - this._isEditFromUs = true; - try { - if (!this._diffInfo.get().changes.includes(change)) { - return false; - } - const edits: ISingleEditOperation[] = []; - for (const edit of change.innerChanges ?? []) { - const newText = this.originalModel.getValueInRange(edit.originalRange); - edits.push(EditOperation.replace(edit.modifiedRange, newText)); - } - this.modifiedModel.pushEditOperations(null, edits, _ => null); - } finally { - this._isEditFromUs = false; - } - await this._updateDiffInfoSeq(); - if (this._diffInfo.get().identical) { - this._stateObs.set(WorkingSetEntryState.Rejected, undefined); - } - return true; - } - - private _applyEdits(edits: ISingleEditOperation[]) { - // make the actual edit - this._isEditFromUs = true; - try { - let result: ISingleEditOperation[] = []; - this.modifiedModel.pushEditOperations(null, edits, (undoEdits) => { - result = undoEdits; - return null; - }); - return result; - } finally { - this._isEditFromUs = false; - } - } - - private async _updateDiffInfoSeq() { - const myDiffOperationId = ++this._diffOperationIds; - await Promise.resolve(this._diffOperation); - if (this._diffOperationIds === myDiffOperationId) { - const thisDiffOperation = this._updateDiffInfo(); - this._diffOperation = thisDiffOperation; - await thisDiffOperation; - } - } - - private async _updateDiffInfo(): Promise { - - if (this.originalModel.isDisposed() || this.modifiedModel.isDisposed()) { - return; - } - - const docVersionNow = this.modifiedModel.getVersionId(); - const snapshotVersionNow = this.originalModel.getVersionId(); - - const ignoreTrimWhitespace = this._diffTrimWhitespace.get(); - - const diff = await this._editorWorkerService.computeDiff( - this.originalModel.uri, - this.modifiedModel.uri, - { ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 }, - 'advanced' - ); - - if (this.originalModel.isDisposed() || this.modifiedModel.isDisposed()) { - return; - } - - // only update the diff if the documents didn't change in the meantime - if (this.modifiedModel.getVersionId() === docVersionNow && this.originalModel.getVersionId() === snapshotVersionNow) { - const diff2 = diff ?? nullDocumentDiff; - this._diffInfo.set(diff2, undefined); - this._edit = OffsetEdits.fromLineRangeMapping(this.originalModel, this.modifiedModel, diff2.changes); - } - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingNotebookEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingNotebookEditorIntegration.ts deleted file mode 100644 index f702df873c8..00000000000 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingNotebookEditorIntegration.ts +++ /dev/null @@ -1,578 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { autorun, derivedWithStore, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; -import { debouncedObservable } from '../../../../../base/common/observableInternal/utils.js'; -import { basename } from '../../../../../base/common/resources.js'; -import { nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; -import { localize } from '../../../../../nls.js'; -import { MenuId } from '../../../../../platform/actions/common/actions.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IResourceDiffEditorInput } from '../../../../common/editor.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { NotebookDeletedCellDecorator } from '../../../notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.js'; -import { NotebookInsertedCellDecorator } from '../../../notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.js'; -import { INotebookTextDiffEditor } from '../../../notebook/browser/diff/notebookDiffEditorBrowser.js'; -import { INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; -import { NotebookCellTextModel } from '../../../notebook/common/model/notebookCellTextModel.js'; -import { NotebookTextModel } from '../../../notebook/common/model/notebookTextModel.js'; -import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; -import { IModifiedFileEntry, IModifiedFileEntryChangeHunk, IModifiedFileEntryEditorIntegration } from '../../common/chatEditingService.js'; -import { ChatEditingCodeEditorIntegration, IDocumentDiff2 } from './chatEditingCodeEditorIntegration.js'; - -/** - * All entries will contain a IDocumentDiff - * Even when there are no changes, diff will contain the number of lines in the document. - * This way we can always calculate the total number of lines in the document. - */ -export type ICellDiffInfo = { - originalCellIndex: number; - modifiedCellIndex: number; - type: 'unchanged'; - diff: IDocumentDiff2; // Null diff Change (property to be consistent with others, also we have a list of all line numbers) -} | { - originalCellIndex: number; - modifiedCellIndex: number; - type: 'modified'; - diff: IDocumentDiff2; // List of the changes. -} | -{ - modifiedCellIndex: undefined; - originalCellIndex: number; - type: 'delete'; - diff: IDocumentDiff2; // List of all the lines deleted. -} | -{ - modifiedCellIndex: number; - originalCellIndex: undefined; - type: 'insert'; - diff: IDocumentDiff2; // List of all the new lines. -}; - - -export class ChatEditingNotebookEditorIntegration extends Disposable implements IModifiedFileEntryEditorIntegration { - private readonly _currentIndex = observableValue(this, -1); - readonly currentIndex: IObservable = this._currentIndex; - - // TODO@DonJayamanne For now we're going to ignore being able to focus on a deleted cell. - private readonly _currentCell = observableValue(this, undefined); - readonly currentCell: IObservable = this._currentCell; - - private readonly _currentChange = observableValue<{ change: ICellDiffInfo; index: number } | undefined>(this, undefined); - readonly currentChange: IObservable<{ change: ICellDiffInfo; index: number } | undefined> = this._currentChange; - - private readonly cellEditorIntegrations = new Map }>(); - - private readonly insertDeleteDecorators: IObservable<{ insertedCellDecorator: NotebookInsertedCellDecorator; deletedCellDecorator: NotebookDeletedCellDecorator } | undefined>; - - constructor( - private readonly _entry: IModifiedFileEntry, - private readonly notebookEditor: INotebookEditor, - private readonly notebookModel: NotebookTextModel, - originalModel: NotebookTextModel, - private readonly cellChanges: IObservable, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IEditorService private readonly _editorService: IEditorService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, - ) { - super(); - - const onDidChangeVisibleRanges = debouncedObservable(observableFromEvent(notebookEditor.onDidChangeVisibleRanges, () => notebookEditor.visibleRanges), 50); - const notebookEdotirViewModelAttached = observableFromEvent(notebookEditor.onDidAttachViewModel, () => notebookEditor.getViewModel()); - - - // INIT current index when: enabled, not streaming anymore, once per request, and when having changes - let lastModifyingRequestId: string | undefined; - this._store.add(autorun(r => { - - if (!_entry.isCurrentlyBeingModifiedBy.read(r) - && lastModifyingRequestId !== _entry.lastModifyingRequestId - && cellChanges.read(r).some(c => c.type !== 'unchanged' && c.type !== 'delete' && !c.diff.identical && !c.diff.identical) - ) { - lastModifyingRequestId = _entry.lastModifyingRequestId; - const firstModifiedCell = cellChanges.read(r). - filter(c => c.type !== 'unchanged' && c.type !== 'delete'). - filter(c => !c.diff.identical). - reduce((prev, curr) => curr.modifiedCellIndex < prev ? curr.modifiedCellIndex : prev, Number.MAX_SAFE_INTEGER); - if (typeof firstModifiedCell !== 'number' || firstModifiedCell === Number.MAX_SAFE_INTEGER) { - return; - } - const activeCell = notebookEditor.getActiveCell(); - const index = activeCell ? notebookModel.cells.findIndex(c => c.handle === activeCell.handle) : firstModifiedCell; - this._currentCell.set(notebookModel.cells[index], undefined); - } - })); - - this._register(autorun(r => { - const sortedCellChanges = sortCellChanges(cellChanges.read(r)); - - const changes = sortedCellChanges.filter(c => c.type !== 'unchanged' && c.type !== 'delete' && !c.diff.identical); - onDidChangeVisibleRanges.read(r); - if (!changes.length) { - this.cellEditorIntegrations.forEach(({ diff }) => { - diff.set({ ...diff.get(), ...nullDocumentDiff }, undefined); - }); - return; - } - - const validCells = new Set(); - changes.forEach((diff) => { - if (diff.modifiedCellIndex === undefined) { - return; - } - const cell = notebookModel.cells[diff.modifiedCellIndex]; - const editor = notebookEditor.codeEditors.find(([vm,]) => vm.handle === notebookModel.cells[diff.modifiedCellIndex].handle)?.[1]; - if (!editor || !cell) { - return; - } - validCells.add(cell); - if (this.cellEditorIntegrations.has(cell)) { - this.cellEditorIntegrations.get(cell)!.diff.set(diff.diff, undefined); - } else { - const diff2 = observableValue(`diff${cell.handle}`, diff.diff); - const integration = this.instantiationService.createInstance(ChatEditingCodeEditorIntegration, _entry, editor, diff2); - this.cellEditorIntegrations.set(cell, { integration, diff: diff2 }); - this._register(integration); - this._register(editor.onDidDispose(() => { - this.cellEditorIntegrations.get(cell)?.integration.dispose(); - this.cellEditorIntegrations.delete(cell); - })); - this._register(editor.onDidChangeModel(() => { - if (editor.getModel() !== cell.textModel) { - this.cellEditorIntegrations.get(cell)?.integration.dispose(); - this.cellEditorIntegrations.delete(cell); - } - })); - } - }); - - // Dispose old integrations as the editors are no longer valid. - this.cellEditorIntegrations.forEach((v, cell) => { - if (!validCells.has(cell)) { - v.integration.dispose(); - this.cellEditorIntegrations.delete(cell); - } - }); - - // set initial index - this._currentIndex.set(0, undefined); - this._revealChange(sortedCellChanges[0]); - - this._register(autorun(r => { - const currentChange = this.currentChange.read(r); - if (currentChange) { - const change = currentChange.change; - const indexInChange = currentChange.index; - const diffChangeIndex = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged' && !c.diff.identical)).findIndex(c => c === change); - - if (diffChangeIndex !== -1) { - this._currentIndex.set(diffChangeIndex + indexInChange, undefined); - } - } else { - this._currentIndex.set(-1, undefined); - } - })); - })); - - this.insertDeleteDecorators = derivedWithStore((r, store) => { - if (!notebookEdotirViewModelAttached.read(r)) { - return; - } - - const insertedCellDecorator = store.add(this.instantiationService.createInstance(NotebookInsertedCellDecorator, this.notebookEditor)); - const deletedCellDecorator = store.add(this.instantiationService.createInstance(NotebookDeletedCellDecorator, this.notebookEditor, { - className: 'chat-diff-change-content-widget', - telemetrySource: 'chatEditingNotebookHunk', - menuId: MenuId.ChatEditingEditorHunk, - argFactory: (deletedCellIndex: number) => { - return { - accept() { - const entry = cellChanges.get().find(c => c.type === 'delete' && c.originalCellIndex === deletedCellIndex)?.diff; - if (entry) { - return entry.keep(entry.changes[0]); - } - return Promise.resolve(true); - }, - reject() { - const entry = cellChanges.get().find(c => c.type === 'delete' && c.originalCellIndex === deletedCellIndex)?.diff; - if (entry) { - return entry.undo(entry.changes[0]); - } - return Promise.resolve(true); - }, - } satisfies IModifiedFileEntryChangeHunk; - } - })); - - return { - insertedCellDecorator, - deletedCellDecorator - }; - }); - - this._register(autorun(r => { - // We can have inserted cells that have been accepted, in those cases we do not wany any decorators on them. - const changes = cellChanges.read(r).filter(c => c.type === 'insert' ? !c.diff.identical : true); - const decorators = this.insertDeleteDecorators.read(r); - if (decorators) { - decorators.insertedCellDecorator.apply(changes); - decorators.deletedCellDecorator.apply(changes, originalModel); - } - })); - } - - getCurrentCell() { - const activeCell = this.notebookModel.cells.find(c => c.handle === this.notebookEditor.getActiveCell()?.handle) || this._currentCell.get(); - if (!activeCell) { - return undefined; - } - const index = this.notebookModel.cells.findIndex(c => c.handle === activeCell.handle); - const integration = this.cellEditorIntegrations.get(activeCell)?.integration; - return integration ? { integration, index: index, handle: activeCell.handle, cell: activeCell } : undefined; - } - - selectCell(cell: NotebookCellTextModel) { - const integration = this.cellEditorIntegrations.get(cell)?.integration; - if (integration) { - this._currentCell.set(cell, undefined); - const cellViewModel = this.notebookEditor.getViewModel()?.viewCells.find(c => c.handle === cell.handle); - if (cellViewModel) { - this.notebookEditor.focusNotebookCell(cellViewModel, 'editor'); - } - } - } - getNextCell(nextOrPrevious: boolean) { - const current = this.getCurrentCell(); - if (!current) { - // const changes = this.cellChanges.get().filter(c => c.type === 'modified' || c.type !== 'delete'); - // if (!changes.length) { - // return undefined; - // } - // return this.getIntegrationForCell(changes[0].modifiedCellIndex); - return; - } - const changes = this.cellChanges.get().filter(c => c.type === 'modified' || c.type === 'insert'); - const nextIndex = changes.reduce((prev, curr) => { - if (nextOrPrevious) { - if (typeof curr.modifiedCellIndex !== 'number' || curr.modifiedCellIndex <= current.index) { - return prev; - } - return Math.min(prev, curr.modifiedCellIndex); - } else { - if (typeof curr.modifiedCellIndex !== 'number' || curr.modifiedCellIndex >= current.index) { - return prev; - } - return Math.max(prev, curr.modifiedCellIndex); - } - }, nextOrPrevious ? Number.MAX_SAFE_INTEGER : -1); - if (nextIndex === -1 || nextIndex === Number.MAX_SAFE_INTEGER) { - return undefined; - } - return this.getCell(nextIndex); - } - - getCell(modifiedCellIndex: number) { - const cell = this.notebookModel.cells[modifiedCellIndex]; - const integration = this.cellEditorIntegrations.get(cell)?.integration; - return integration ? { integration, index: modifiedCellIndex, handle: cell.handle, cell } : undefined; - } - - reveal(firstOrLast: boolean): void { - const changes = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged')); - if (!changes.length) { - return undefined; - } - const change = firstOrLast ? changes[0] : changes[changes.length - 1]; - this._revealChange(change, firstOrLast); - } - - private _revealChange(change: ICellDiffInfo, firstOrLast?: boolean) { - switch (change.type) { - case 'insert': - case 'modified': - { - const cell = this.getCell(change.modifiedCellIndex); - if (!cell) { - return false; - } - - cell.integration.reveal(firstOrLast ?? true); - this._currentChange.set({ change: change, index: cell.integration.currentIndex.get() }, undefined); - - return true; - } - case 'delete': - // reveal the deleted cell decorator - this._currentCell.set(undefined, undefined); - this.insertDeleteDecorators.get()?.deletedCellDecorator.reveal(change.originalCellIndex); - this._currentChange.set({ change: change, index: 0 }, undefined); - return true; - default: - break; - } - - return false; - } - - next(wrap: boolean): boolean { - const changes = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged')); - const currentChange = this.currentChange.get(); - if (!currentChange) { - const firstChange = changes[0]; - - if (firstChange) { - this._currentCell.set(undefined, undefined); - return this._revealChange(firstChange); - } - - return false; - } - - // go to next - // first check if we are at the end of the current change - switch (currentChange.change.type) { - case 'modified': - { - const currentChangeInfo = this.getCell(currentChange.change.modifiedCellIndex); - if (!currentChangeInfo) { - return false; - } - - if (currentChangeInfo.integration.next(false)) { - this._currentChange.set({ change: currentChange.change, index: currentChangeInfo.integration.currentIndex.get() }, undefined); - return true; - } else { - const nextChange = changes[changes.indexOf(currentChange.change) + 1]; - if (nextChange) { - return this._revealChange(nextChange, true); - } - } - } - break; - case 'insert': - case 'delete': - { - // go to next change directly - const nextChange = changes[changes.indexOf(currentChange.change) + 1]; - if (nextChange) { - return this._revealChange(nextChange, true); - } - } - break; - default: - break; - } - - if (wrap) { - return this.next(false); - } - - return false; - } - - previous(wrap: boolean): boolean { - const changes = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged')); - const currentChange = this.currentChange.get(); - if (!currentChange) { - const lastChange = changes[changes.length - 1]; - if (lastChange) { - this._currentCell.set(undefined, undefined); - return this._revealChange(lastChange, false); - } - - return false; - } - - // go to previous - // first check if we are at the start of the current change - switch (currentChange.change.type) { - case 'modified': - { - const currentChangeInfo = this.getCell(currentChange.change.modifiedCellIndex); - if (!currentChangeInfo) { - return false; - } - - if (currentChangeInfo.integration.previous(false)) { - this._currentChange.set({ change: currentChange.change, index: currentChangeInfo.integration.currentIndex.get() }, undefined); - return true; - } else { - const prevChange = changes[changes.indexOf(currentChange.change) - 1]; - if (prevChange) { - return this._revealChange(prevChange, false); - } - } - } - break; - case 'insert': - case 'delete': - { - // go to previous change directly - const prevChange = changes[changes.indexOf(currentChange.change) - 1]; - if (prevChange) { - return this._revealChange(prevChange, false); - } - } - break; - default: - break; - } - - if (wrap) { - const lastChange = changes[changes.length - 1]; - if (lastChange) { - return this._revealChange(lastChange, false); - } - } - - return false; - } - - enableAccessibleDiffView(): void { - this.getCurrentCell()?.integration.enableAccessibleDiffView(); - } - acceptNearestChange(change: IModifiedFileEntryChangeHunk): void { - change.accept(); - this.next(true); - } - rejectNearestChange(change: IModifiedFileEntryChangeHunk): void { - change.reject(); - this.next(true); - } - async toggleDiff(_change: IModifiedFileEntryChangeHunk | undefined): Promise { - const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName; - const diffInput = { - original: { resource: this._entry.originalURI, options: { selection: undefined } }, - modified: { resource: this._entry.modifiedURI, options: { selection: undefined } }, - label: defaultAgentName - ? localize('diff.agent', '{0} (changes from {1})', basename(this._entry.modifiedURI), defaultAgentName) - : localize('diff.generic', '{0} (changes from chat)', basename(this._entry.modifiedURI)) - } satisfies IResourceDiffEditorInput; - await this._editorService.openEditor(diffInput); - - } -} -export class ChatEditingNotebookDiffEditorIntegration extends Disposable implements IModifiedFileEntryEditorIntegration { - private readonly _currentIndex = observableValue(this, -1); - readonly currentIndex: IObservable = this._currentIndex; - - constructor( - private readonly notebookDiffEditor: INotebookTextDiffEditor, - private readonly cellChanges: IObservable - ) { - super(); - - this._store.add(autorun(r => { - const index = notebookDiffEditor.currentChangedIndex.read(r); - const numberOfCellChanges = cellChanges.read(r).filter(c => !c.diff.identical); - if (numberOfCellChanges.length && index >= 0 && index < numberOfCellChanges.length) { - // Notebook Diff editor only supports navigating through changes to cells. - // However in chat we take changes to lines in the cells into account. - // So if we're on the second cell and first cell has 3 changes, then we're on the 4th change. - const changesSoFar = countChanges(numberOfCellChanges.slice(0, index + 1)); - this._currentIndex.set(changesSoFar - 1, undefined); - } else { - this._currentIndex.set(-1, undefined); - } - })); - } - - reveal(firstOrLast: boolean): void { - const changes = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged')); - if (!changes.length) { - return undefined; - } - if (firstOrLast) { - this.notebookDiffEditor.firstChange(); - } else { - this.notebookDiffEditor.lastChange(); - } - } - - next(_wrap: boolean): boolean { - const changes = this.cellChanges.get().filter(c => !c.diff.identical).length; - if (this.notebookDiffEditor.currentChangedIndex.get() === changes - 1) { - return false; - } - this.notebookDiffEditor.nextChange(); - return true; - } - - previous(_wrap: boolean): boolean { - const changes = this.cellChanges.get().filter(c => !c.diff.identical).length; - if (this.notebookDiffEditor.currentChangedIndex.get() === changes - 1) { - return false; - } - this.notebookDiffEditor.nextChange(); - return true; - } - - enableAccessibleDiffView(): void { - // - } - acceptNearestChange(change: IModifiedFileEntryChangeHunk): void { - change.accept(); - this.next(true); - } - rejectNearestChange(change: IModifiedFileEntryChangeHunk): void { - change.reject(); - this.next(true); - } - async toggleDiff(_change: IModifiedFileEntryChangeHunk | undefined): Promise { - // - } -} - -export function countChanges(changes: ICellDiffInfo[]): number { - return changes.reduce((count, diff) => { - // When we accept some of the cell insert/delete the items might still be in the list. - if (diff.diff.identical) { - return count; - } - switch (diff.type) { - case 'delete': - return count + 1; // We want to see 1 deleted entry in the pill for navigation - case 'insert': - return count + 1; // We want to see 1 new entry in the pill for navigation - case 'modified': - return count + diff.diff.changes.length; - default: - return count; - } - }, 0); - -} - -export function sortCellChanges(changes: ICellDiffInfo[]): ICellDiffInfo[] { - return [...changes].sort((a, b) => { - // For unchanged and modified, use modifiedCellIndex - if ((a.type === 'unchanged' || a.type === 'modified') && - (b.type === 'unchanged' || b.type === 'modified')) { - return a.modifiedCellIndex - b.modifiedCellIndex; - } - - // For delete entries, use originalCellIndex - if (a.type === 'delete' && b.type === 'delete') { - return a.originalCellIndex - b.originalCellIndex; - } - - // For insert entries, use modifiedCellIndex - if (a.type === 'insert' && b.type === 'insert') { - return a.modifiedCellIndex - b.modifiedCellIndex; - } - - if ((a.type === 'delete' && b.type !== 'insert') || (a.type !== 'insert' && b.type === 'delete')) { - return a.originalCellIndex - b.originalCellIndex; - } - // Mixed types: compare based on available indices - const aIndex = a.type === 'delete' ? a.originalCellIndex : - (a.type === 'insert' ? a.modifiedCellIndex : a.modifiedCellIndex); - const bIndex = b.type === 'delete' ? b.originalCellIndex : - (b.type === 'insert' ? b.modifiedCellIndex : b.modifiedCellIndex); - - return aIndex - bIndex; - }); -} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index bbce908e641..40e2e0d81ea 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce, compareBy, delta } from '../../../../../base/common/arrays.js'; -import { findLastIdx } from '../../../../../base/common/arraysFind.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { ErrorNoTelemetry } from '../../../../../base/common/errors.js'; @@ -34,10 +33,11 @@ import { IExtensionService } from '../../../../services/extensions/common/extens import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js'; import { CellUri } from '../../../notebook/common/notebookCommon.js'; -import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; +import { IChatAgentService } from '../../common/chatAgents.js'; import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingResourceContextKey, ChatEditingSessionState, chatEditingSnapshotScheme, IChatEditingService, IChatEditingSession, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, IStreamingEdits, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel, isCellTextEditOperation } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; +import { ChatAgentLocation } from '../../common/constants.js'; import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingSession } from './chatEditingSession.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; @@ -245,11 +245,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic // each of them. Note that text edit groups can be updated // multiple times during the process of response streaming. const editsSeen: ({ seen: number; streaming: IStreamingEdits } | undefined)[] = []; - // Same deal as above, but for code block URIs. Code block URIs preceed a - // text edit group, and are used to allow us to start an editing state - // prior to the code actually streaming in. When a new edit block it seen, - // it'll look back and 'claim' the last unclaimed matchin codeblock URI. - const codeBlockUrisSeen: ({ uri: URI; streaming?: IStreamingEdits } | undefined)[] = []; const editedFilesExist = new ResourceMap>(); const ensureEditorOpen = (partUri: URI) => { @@ -263,7 +258,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return; } const activeUri = this._editorService.activeEditorPane?.input.resource; - const inactive = activeUri && session.workingSet.has(activeUri); + const inactive = Boolean(activeUri && session.entries.get().find(entry => isEqual(activeUri, entry.modifiedURI))); this._editorService.openEditor({ resource: uri, options: { inactive, preserveFocus: true, pinned: true } }); })); }; @@ -295,28 +290,16 @@ export class ChatEditingService extends Disposable implements IChatEditingServic continue; } - if (part.kind === 'codeblockUri') { - ensureEditorOpen(part.uri); - codeBlockUrisSeen[i] ??= { uri: part.uri, streaming: session.startStreamingEdits(part.uri, responseModel, undoStop) }; - continue; - } - if (part.kind !== 'textEditGroup' && part.kind !== 'notebookEditGroup') { continue; } + ensureEditorOpen(part.uri); // get new edits and start editing session let entry = editsSeen[i]; if (!entry) { - const codeBlockIndex = findLastIdx(codeBlockUrisSeen, e => e?.streaming && isEqual(e.uri, part.uri), i - 1); - if (codeBlockIndex !== -1) { - entry = { seen: 0, streaming: codeBlockUrisSeen[codeBlockIndex]!.streaming! }; - codeBlockUrisSeen[codeBlockIndex]!.streaming = undefined; - } else { - entry = { seen: 0, streaming: session.startStreamingEdits(CellUri.parse(part.uri)?.notebook ?? part.uri, responseModel, undoStop) }; - } - + entry = { seen: 0, streaming: session.startStreamingEdits(CellUri.parse(part.uri)?.notebook ?? part.uri, responseModel, undoStop) }; editsSeen[i] = entry; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index ca4ccb33331..714f256b7be 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -10,9 +10,8 @@ import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter } from '../../../../../base/common/event.js'; import { StringSHA1 } from '../../../../../base/common/hash.js'; import { Iterable } from '../../../../../base/common/iterator.js'; -import { Disposable, DisposableMap, DisposableStore, dispose } from '../../../../../base/common/lifecycle.js'; -import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; -import { Schemas } from '../../../../../base/common/network.js'; +import { Disposable, DisposableMap, dispose } from '../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; import { asyncTransaction, autorun, derived, derivedOpts, derivedWithStore, IObservable, IReader, ITransaction, ObservablePromise, observableValue, transaction } from '../../../../../base/common/observable.js'; import { autorunDelta, autorunIterableDelta } from '../../../../../base/common/observableInternal/autorun.js'; import { isEqual, joinPath } from '../../../../../base/common/resources.js'; @@ -50,6 +49,8 @@ import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentE import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { ChatEditingModifiedNotebookDiff } from './notebook/chatEditingModifiedNotebookDiff.js'; const STORAGE_CONTENTS_FOLDER = 'contents'; const STORAGE_STATE_FILE = 'state.json'; @@ -77,7 +78,7 @@ class ThrottledSequencer extends Sequencer { const p1 = promiseTask(); const p2 = noDelay ? Promise.resolve(undefined) - : timeout(this._minDuration); + : timeout(this._minDuration, CancellationToken.None); const [result] = await Promise.all([p1, p2]); return result; @@ -140,19 +141,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private _workingSet = new ResourceMap(); - get workingSet() { - this._assertNotDisposed(); - - // Return here a reunion between the AI modified entries and the user built working set - const result = new ResourceMap(this._workingSet); - for (const entry of this._entriesObs.get()) { - result.set(entry.modifiedURI, { state: entry.state.get() }); - } - - return result; - } - - private _removedTransientEntries = new ResourceSet(); private _editorPane: MultiDiffEditor | undefined; @@ -351,8 +339,13 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return; } - entriesContent.read(reader); // trigger re-diffing when contents change + const entries = entriesContent.read(reader); // trigger re-diffing when contents change + if (entries?.before && ChatEditingModifiedNotebookEntry.canHandleSnapshot(entries.before)) { + const diffService = this._instantiationService.createInstance(ChatEditingModifiedNotebookDiff, entries.before, entries.after); + return new ObservablePromise(diffService.computeDiff()); + + } const ignoreTrimWhitespace = this._ignoreTrimWhitespaceObservable.read(reader); const promise = this._editorWorkerService.computeDiff( refs[0].object.textEditorModel.uri, @@ -404,7 +397,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return [entriesValue.before.snapshotUri, entriesValue.after.snapshotUri]; }); - // todo@DonJayamanne support notebooks here too const diff = this._entryDiffBetweenTextStops(entries, modelUrisObservable); return derived(reader => { @@ -425,10 +417,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio public createSnapshot(requestId: string, undoStop: string | undefined): void { const snapshot = this._createSnapshot(requestId, undoStop); - for (const [uri, data] of this._workingSet) { - if (data.state !== WorkingSetEntryState.Suggested) { - this._workingSet.set(uri, { state: WorkingSetEntryState.Sent, isMarkedReadonly: data.isMarkedReadonly }); - } + for (const [uri, _] of this._workingSet) { + this._workingSet.set(uri, { state: WorkingSetEntryState.Sent }); } const linearHistoryPtr = this._linearHistoryIndex.get(); @@ -469,15 +459,15 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }; } - public async getSnapshotModel(requestId: string, undoStop: string | undefined, snapshotUri: URI): Promise { + public getSnapshot(requestId: string, undoStop: string | undefined, snapshotUri: URI): ISnapshotEntry | undefined { const entries = undoStop === POST_EDIT_STOP_ID ? this._findSnapshot(requestId)?.postEdit : this._findEditStop(requestId, undoStop)?.stop.entries; - if (!entries) { - return null; - } + return entries && [...entries.values()].find((e) => isEqual(e.snapshotUri, snapshotUri)); + } - const snapshotEntry = [...entries.values()].find((e) => isEqual(e.snapshotUri, snapshotUri)); + public async getSnapshotModel(requestId: string, undoStop: string | undefined, snapshotUri: URI): Promise { + const snapshotEntry = this.getSnapshot(requestId, undoStop, snapshotUri); if (!snapshotEntry) { return null; } @@ -556,9 +546,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const state = this._workingSet.get(uri); if (state !== undefined) { didRemoveUris = this._workingSet.delete(uri) || didRemoveUris; - if (reason === WorkingSetEntryRemovalReason.User && state.state === WorkingSetEntryState.Suggested) { - this._removedTransientEntries.add(uri); - } } } @@ -569,22 +556,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); } - markIsReadonly(resource: URI, isReadonly?: boolean): void { - const entry = this._workingSet.get(resource); - if (entry) { - if (entry.state === WorkingSetEntryState.Suggested) { - entry.state = WorkingSetEntryState.Attached; - } - entry.isMarkedReadonly = isReadonly ?? !entry.isMarkedReadonly; - } else { - this._workingSet.set(resource, { - state: WorkingSetEntryState.Attached, - isMarkedReadonly: isReadonly ?? true - }); - } - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - } - private _assertNotDisposed(): void { if (this._state.get() === ChatEditingSessionState.Disposed) { throw new BugIndicatingError(`Cannot access a disposed editing session`); @@ -755,61 +726,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }; } - private _trackUntitledWorkingSetEntry(resource: URI) { - if (resource.scheme !== Schemas.untitled) { - return; - } - const untitled = this._textFileService.untitled.get(resource); - if (!untitled) { // Shouldn't happen - return; - } - - // Track this file until - // 1. it is removed from the working set - // 2. it is closed - // 3. we are disposed - const store = new DisposableStore(); - store.add(this.onDidChange(e => { - if (e === ChatEditingSessionChangeType.WorkingSet && !this._workingSet.get(resource)) { - // The user has removed the file from the working set - store.dispose(); - } - })); - store.add(this._textFileService.untitled.onDidSave(e => { - const existing = this._workingSet.get(resource); - if (isEqual(e.source, resource) && existing) { - this._workingSet.delete(resource); - this._workingSet.set(e.target, existing); - store.dispose(); - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - } - })); - store.add(this._editorService.onDidCloseEditor((e) => { - if (isEqual(e.editor.resource, resource)) { - this._workingSet.delete(resource); - store.dispose(); - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - } - })); - this._store.add(store); - } - - addFileToWorkingSet(resource: URI, description?: string, proposedState?: WorkingSetEntryState.Suggested): void { - const state = this._workingSet.get(resource); - if (proposedState === WorkingSetEntryState.Suggested) { - if (state !== undefined || this._removedTransientEntries.has(resource)) { - return; - } - this._workingSet.set(resource, { description, state: WorkingSetEntryState.Suggested }); - this._trackUntitledWorkingSetEntry(resource); - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - } else if (state === undefined || state.state === WorkingSetEntryState.Suggested) { - this._workingSet.set(resource, { description, state: WorkingSetEntryState.Attached }); - this._trackUntitledWorkingSetEntry(resource); - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - } - } - private _getHistoryEntryByLinearIndex(index: number) { const history = this._linearHistory.get(); const searchedIndex = binarySearch2(history.length, (e) => history[e].startIndex - index); @@ -1041,11 +957,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, mustExist = false, initialContent: string | undefined): Promise { const multiDiffEntryDelegate = { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }; const chatKind = mustExist ? ChatEditKind.Created : ChatEditKind.Modified; + const notebookUri = CellUri.parse(resource)?.notebook || resource; try { - const notebookUri = CellUri.parse(resource)?.notebook || resource; // If a notebook isn't open, then use the old synchronization approach. - if (this._notebookService.hasSupportedNotebooks(notebookUri) && (this._notebookService.getNotebookTextModel(notebookUri) || ChatEditingModifiedNotebookEntry.canHandleSnapshot(initialContent))) { - return ChatEditingModifiedNotebookEntry.create(notebookUri, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent, this._instantiationService); + if (this._notebookService.hasSupportedNotebooks(notebookUri) && (this._notebookService.getNotebookTextModel(notebookUri) || ChatEditingModifiedNotebookEntry.canHandleSnapshotContent(initialContent))) { + return await ChatEditingModifiedNotebookEntry.create(notebookUri, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent, this._instantiationService); } else { const ref = await this._textModelService.createModelReference(resource); return this._instantiationService.createInstance(ChatEditingModifiedDocumentEntry, ref, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent); @@ -1057,7 +973,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio // this file does not exist yet, create it and try again await this._bulkEditService.apply({ edits: [{ newResource: resource }] }); this._editorService.openEditor({ resource, options: { inactive: true, preserveFocus: true, pinned: true } }); - return this._createModifiedFileEntry(resource, telemetryInfo, true, initialContent); + if (this._notebookService.hasSupportedNotebooks(notebookUri)) { + return await ChatEditingModifiedNotebookEntry.create(resource, multiDiffEntryDelegate, telemetryInfo, ChatEditKind.Created, initialContent, this._instantiationService); + } else { + return this._createModifiedFileEntry(resource, telemetryInfo, true, initialContent); + } } } @@ -1149,7 +1069,7 @@ class ChatEditingSessionStorage { this._logService.debug(`chatEditingSession: Restoring editing session at ${stateFilePath.toString()}`); const stateFileContent = await this._fileService.readFile(stateFilePath); const data = JSON.parse(stateFileContent.value.toString()) as IChatEditingSessionDTO; - if (data.version !== STORAGE_VERSION) { + if (!COMPATIBLE_STORAGE_VERSIONS.includes(data.version)) { return undefined; } @@ -1346,7 +1266,8 @@ interface IModifiedEntryTelemetryInfoDTO { type ResourceMapDTO = [string, T][]; -const STORAGE_VERSION = 1; +const COMPATIBLE_STORAGE_VERSIONS = [1, 2]; +const STORAGE_VERSION = 2; /** Old history uses IChatEditingSessionSnapshotDTO, new history uses IChatEditingSessionSnapshotDTO. */ interface IChatEditingSessionDTO { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookDiff.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookDiff.ts new file mode 100644 index 00000000000..5521a153318 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookDiff.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { computeDiff } from '../../../../notebook/common/notebookDiff.js'; +import { INotebookEditorModelResolverService } from '../../../../notebook/common/notebookEditorModelResolverService.js'; +import { INotebookLoggingService } from '../../../../notebook/common/notebookLoggingService.js'; +import { INotebookEditorWorkerService } from '../../../../notebook/common/services/notebookWorkerService.js'; +import { IEditSessionEntryDiff } from '../../../common/chatEditingService.js'; +import { ISnapshotEntry } from '../chatEditingModifiedFileEntry.js'; + +export class ChatEditingModifiedNotebookDiff { + static NewModelCounter: number = 0; + constructor( + private readonly original: ISnapshotEntry, + private readonly modified: ISnapshotEntry, + @INotebookEditorWorkerService private readonly notebookEditorWorkerService: INotebookEditorWorkerService, + @INotebookLoggingService private readonly notebookLoggingService: INotebookLoggingService, + @INotebookEditorModelResolverService private readonly notebookEditorModelService: INotebookEditorModelResolverService, + ) { + + } + + async computeDiff(): Promise { + + let added = 0; + let removed = 0; + + const disposables = new DisposableStore(); + try { + const [modifiedRef, originalRef] = await Promise.all([ + this.notebookEditorModelService.resolve(this.modified.snapshotUri), + this.notebookEditorModelService.resolve(this.original.snapshotUri) + ]); + disposables.add(modifiedRef); + disposables.add(originalRef); + const notebookDiff = await this.notebookEditorWorkerService.computeDiff(this.original.snapshotUri, this.modified.snapshotUri); + const result = computeDiff(originalRef.object.notebook, modifiedRef.object.notebook, notebookDiff); + result.cellDiffInfo.forEach(diff => { + switch (diff.type) { + case 'modified': + case 'insert': + added++; + break; + case 'delete': + removed++; + break; + default: + break; + } + }); + } catch (e) { + this.notebookLoggingService.error('Notebook Chat', 'Error computing diff:\n' + e); + } finally { + disposables.dispose(); + } + + return { + added, + removed, + identical: added === 0 && removed === 0, + quitEarly: false, + modifiedURI: this.modified.snapshotUri, + originalURI: this.original.snapshotUri, + }; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookSnapshot.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookSnapshot.ts new file mode 100644 index 00000000000..5066d63b7c1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookSnapshot.ts @@ -0,0 +1,182 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { decodeBase64, encodeBase64, VSBuffer } from '../../../../../../base/common/buffer.js'; +import { filter } from '../../../../../../base/common/objects.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { SnapshotContext } from '../../../../../services/workingCopy/common/fileWorkingCopy.js'; +import { NotebookCellTextModel } from '../../../../notebook/common/model/notebookCellTextModel.js'; +import { NotebookTextModel } from '../../../../notebook/common/model/notebookTextModel.js'; +import { CellEditType, ICellDto2, ICellEditOperation, IOutputItemDto, NotebookData, NotebookSetting, TransientOptions } from '../../../../notebook/common/notebookCommon.js'; + +const BufferMarker = 'ArrayBuffer-4f56482b-5a03-49ba-8356-210d3b0c1c3d'; + +type ChatEditingSnapshotNotebookContentQueryData = { sessionId: string; requestId: string | undefined; undoStop: string | undefined; viewType: string }; +export const ChatEditingNotebookSnapshotScheme = 'chat-editing-notebook-snapshot-model'; + +export function getNotebookSnapshotFileURI(chatSessionId: string, requestId: string | undefined, undoStop: string | undefined, path: string, viewType: string): URI { + return URI.from({ + scheme: ChatEditingNotebookSnapshotScheme, + path, + query: JSON.stringify({ sessionId: chatSessionId, requestId: requestId ?? '', undoStop: undoStop ?? '', viewType } satisfies ChatEditingSnapshotNotebookContentQueryData), + }); +} + +export function parseNotebookSnapshotFileURI(resource: URI): ChatEditingSnapshotNotebookContentQueryData { + const data: ChatEditingSnapshotNotebookContentQueryData = JSON.parse(resource.query); + return { sessionId: data.sessionId ?? '', requestId: data.requestId ?? '', undoStop: data.undoStop ?? '', viewType: data.viewType }; +} + +export function createSnapshot(notebook: NotebookTextModel, transientOptions: TransientOptions | undefined, configurationService: IConfigurationService): string { + const outputSizeLimit = configurationService.getValue(NotebookSetting.outputBackupSizeLimit) * 1024; + return serializeSnapshot(notebook.createSnapshot({ context: SnapshotContext.Backup, outputSizeLimit, transientOptions }), transientOptions); +} + +export function restoreSnapshot(notebook: NotebookTextModel, snapshot: string): void { + try { + const { transientOptions, data } = deserializeSnapshot(snapshot); + notebook.restoreSnapshot(data, transientOptions); + const edits: ICellEditOperation[] = []; + data.cells.forEach((cell, index) => { + const cellId = cell.internalMetadata?.cellId; + if (cellId) { + edits.push({ editType: CellEditType.PartialInternalMetadata, index, internalMetadata: { cellId } }); + } + }); + notebook.applyEdits(edits, true, undefined, () => undefined, undefined, false); + } + catch (ex) { + console.error('Error restoring Notebook snapshot', ex); + } +} + +export class SnapshotComparer { + private readonly data: NotebookData; + private readonly transientOptions: TransientOptions | undefined; + constructor(initialCotent: string) { + this.transientOptions = deserializeSnapshot(initialCotent).transientOptions; + this.data = deserializeSnapshot(initialCotent).data; + } + + isEqual(notebook: NotebookData | NotebookTextModel): boolean { + if (notebook.cells.length !== this.data.cells.length) { + return false; + } + const transientDocumentMetadata = this.transientOptions?.transientDocumentMetadata || {}; + const notebookMetadata = filter(notebook.metadata || {}, key => !transientDocumentMetadata[key]); + const comparerMetadata = filter(this.data.metadata || {}, key => !transientDocumentMetadata[key]); + // When comparing ignore transient items. + if (JSON.stringify(notebookMetadata) !== JSON.stringify(comparerMetadata)) { + return false; + } + const transientCellMetadata = this.transientOptions?.transientCellMetadata || {}; + for (let i = 0; i < notebook.cells.length; i++) { + const notebookCell = notebook.cells[i]; + const comparerCell = this.data.cells[i]; + if (notebookCell instanceof NotebookCellTextModel) { + if (!notebookCell.fastEqual(comparerCell, true)) { + return false; + } + } else { + if (notebookCell.cellKind !== comparerCell.cellKind) { + return false; + } + if (notebookCell.language !== comparerCell.language) { + return false; + } + if (notebookCell.mime !== comparerCell.mime) { + return false; + } + if (notebookCell.source !== comparerCell.source) { + return false; + } + if (!this.transientOptions?.transientOutputs && notebookCell.outputs.length !== comparerCell.outputs.length) { + return false; + } + // When comparing ignore transient items. + const cellMetadata = filter(notebookCell.metadata || {}, key => !transientCellMetadata[key]); + const comparerCellMetadata = filter(comparerCell.metadata || {}, key => !transientCellMetadata[key]); + if (JSON.stringify(cellMetadata) !== JSON.stringify(comparerCellMetadata)) { + return false; + } + + // When comparing ignore transient items. + if (JSON.stringify(sanitizeCellDto2(notebookCell, true, this.transientOptions)) !== JSON.stringify(sanitizeCellDto2(comparerCell, true, this.transientOptions))) { + return false; + } + } + } + + return true; + } +} + +function sanitizeCellDto2(cell: ICellDto2, ignoreInternalMetadata?: boolean, transientOptions?: TransientOptions): ICellDto2 { + const transientCellMetadata = transientOptions?.transientCellMetadata || {}; + const outputs = transientOptions?.transientOutputs ? [] : cell.outputs.map(output => { + // Ensure we're in full control of the data being stored. + // Possible we have classes instead of plain objects. + return { + outputId: output.outputId, + metadata: output.metadata, + outputs: output.outputs.map(item => { + return { + data: item.data, + mime: item.mime, + } satisfies IOutputItemDto; + }), + }; + }); + // Ensure we're in full control of the data being stored. + // Possible we have classes instead of plain objects. + return { + cellKind: cell.cellKind, + language: cell.language, + metadata: cell.metadata ? filter(cell.metadata, key => !transientCellMetadata[key]) : cell.metadata, + outputs, + mime: cell.mime, + source: cell.source, + collapseState: cell.collapseState, + internalMetadata: ignoreInternalMetadata ? undefined : cell.internalMetadata + } satisfies ICellDto2; +} + +function serializeSnapshot(data: NotebookData, transientOptions: TransientOptions | undefined): string { + const dataDto: NotebookData = { + // Never pass transient options, as we're after a backup here. + // Else we end up stripping outputs from backups. + // Whether its persisted or not is up to the serializer. + // However when reloading/restoring we need to preserve outputs. + cells: data.cells.map(cell => sanitizeCellDto2(cell)), + metadata: data.metadata, + }; + return JSON.stringify([ + JSON.stringify(transientOptions) + , JSON.stringify(dataDto, (_key, value) => { + if (value instanceof VSBuffer) { + return { + type: BufferMarker, + data: encodeBase64(value) + }; + } + return value; + }) + ]); +} + +export function deserializeSnapshot(snapshot: string): { transientOptions: TransientOptions | undefined; data: NotebookData } { + const [transientOptionsStr, dataStr] = JSON.parse(snapshot); + const transientOptions = transientOptionsStr ? JSON.parse(transientOptionsStr) as TransientOptions : undefined; + + const data: NotebookData = JSON.parse(dataStr, (_key, value) => { + if (value && value.type === BufferMarker) { + return decodeBase64(value.data); + } + return value; + }); + + return { transientOptions, data }; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNewNotebookContentEdits.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNewNotebookContentEdits.ts new file mode 100644 index 00000000000..b516adce76e --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNewNotebookContentEdits.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { TextEdit } from '../../../../../../editor/common/languages.js'; +import { NotebookTextModel } from '../../../../notebook/common/model/notebookTextModel.js'; +import { CellEditType, ICellEditOperation } from '../../../../notebook/common/notebookCommon.js'; +import { INotebookService } from '../../../../notebook/common/notebookService.js'; + + +/** + * When asking LLM to generate a new notebook, LLM might end up generating the notebook + * using the raw file format. + * E.g. assume we ask LLM to generate a new Github Issues notebook, LLM might end up + * genrating the notebook using the JSON format of github issues file. + * Such a format is not known to copilot extension and those are sent over as regular + * text edits for the Notebook URI. + * + * In such cases we should accumulate all of the edits, generate the content and deserialize the content + * into a notebook, then generate notebooke edits to insert these cells. + */ +export class ChatEditingNewNotebookContentEdits { + private readonly textEdits: TextEdit[] = []; + constructor( + private readonly notebook: NotebookTextModel, + @INotebookService private readonly _notebookService: INotebookService, + ) { + } + + acceptTextEdits(edits: TextEdit[]): void { + if (edits.length) { + this.textEdits.push(...edits); + } + } + + async generateEdits(): Promise { + if (this.notebook.cells.length) { + console.error(`Notebook edits not generated as notebook already has cells`); + return []; + } + const content = this.generateContent(); + if (!content) { + return []; + } + + const notebookEdits: ICellEditOperation[] = []; + try { + const { serializer } = await this._notebookService.withNotebookDataProvider(this.notebook.viewType); + const data = await serializer.dataToNotebook(VSBuffer.fromString(content)); + for (let i = 0; i < data.cells.length; i++) { + notebookEdits.push({ + editType: CellEditType.Replace, + index: i, + count: 0, + cells: [data.cells[i]] + }); + } + } catch (ex) { + console.error(`Failed to generate notebook edits from text edits ${content}`, ex); + return []; + } + + return notebookEdits; + } + + private generateContent() { + try { + return applyTextEdits(this.textEdits); + } catch (ex) { + console.error('Failed to generate content from text edits', ex); + return ''; + } + } +} + +function applyTextEdits(edits: TextEdit[]): string { + let output = ''; + for (const edit of edits) { + output = output.slice(0, edit.range.startColumn) + + edit.text + + output.slice(edit.range.endColumn); + } + return output; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts new file mode 100644 index 00000000000..9418dc7c6bd --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts @@ -0,0 +1,345 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from '../../../../../../base/common/async.js'; +import { DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { ITransaction, IObservable, observableValue, autorun, transaction } from '../../../../../../base/common/observable.js'; +import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; +import { themeColorFromId } from '../../../../../../base/common/themables.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { EditOperation, ISingleEditOperation } from '../../../../../../editor/common/core/editOperation.js'; +import { OffsetEdit } from '../../../../../../editor/common/core/offsetEdit.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { IDocumentDiff, nullDocumentDiff } from '../../../../../../editor/common/diff/documentDiffProvider.js'; +import { DetailedLineRangeMapping } from '../../../../../../editor/common/diff/rangeMapping.js'; +import { TextEdit } from '../../../../../../editor/common/languages.js'; +import { IModelDeltaDecoration, ITextModel, MinimapPosition, OverviewRulerLane } from '../../../../../../editor/common/model.js'; +import { ModelDecorationOptions } from '../../../../../../editor/common/model/textModel.js'; +import { OffsetEdits } from '../../../../../../editor/common/model/textModelOffsetEdit.js'; +import { IEditorWorkerService } from '../../../../../../editor/common/services/editorWorker.js'; +import { IModelContentChangedEvent } from '../../../../../../editor/common/textModelEvents.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { observableConfigValue } from '../../../../../../platform/observable/common/platformObservableUtils.js'; +import { editorSelectionBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; +import { CellEditState } from '../../../../notebook/browser/notebookBrowser.js'; +import { INotebookEditorService } from '../../../../notebook/browser/services/notebookEditorService.js'; +import { NotebookCellTextModel } from '../../../../notebook/common/model/notebookCellTextModel.js'; +import { WorkingSetEntryState } from '../../../common/chatEditingService.js'; +import { IChatResponseModel } from '../../../common/chatModel.js'; +import { pendingRewriteMinimap } from '../chatEditingModifiedFileEntry.js'; + + +/** + * This is very closely similar to the ChatEditingModifiedDocumentEntry class. + * Most of the code has been borrowed from there, as a cell is effectively a document. + * Hence most of the same functionality applies. + */ +export class ChatEditingNotebookCellEntry extends ObservableDisposable { + private static readonly _lastEditDecorationOptions = ModelDecorationOptions.register({ + isWholeLine: true, + description: 'chat-last-edit', + className: 'chat-editing-last-edit-line', + marginClassName: 'chat-editing-last-edit', + overviewRuler: { + position: OverviewRulerLane.Full, + color: themeColorFromId(editorSelectionBackground) + }, + }); + + private static readonly _pendingEditDecorationOptions = ModelDecorationOptions.register({ + isWholeLine: true, + description: 'chat-pending-edit', + className: 'chat-editing-pending-edit', + minimap: { + position: MinimapPosition.Inline, + color: themeColorFromId(pendingRewriteMinimap) + } + }); + + + private _edit: OffsetEdit = OffsetEdit.empty; + private _isEditFromUs: boolean = false; + public get isEditFromUs(): boolean { + return this._isEditFromUs; + } + + private _allEditsAreFromUs: boolean = true; + public get allEditsAreFromUs(): boolean { + return this._allEditsAreFromUs; + } + private _diffOperation: Promise | undefined; + private _diffOperationIds: number = 0; + + private readonly _diffInfo = observableValue(this, nullDocumentDiff); + public get diffInfo(): IObservable { + return this._diffInfo; + } + private readonly _maxModifiedLineNumber = observableValue(this, 0); + readonly maxModifiedLineNumber = this._maxModifiedLineNumber; + + private readonly _editDecorationClear = this._register(new RunOnceScheduler(() => { this._editDecorations = this.modifiedModel.deltaDecorations(this._editDecorations, []); }, 500)); + private _editDecorations: string[] = []; + + private readonly _diffTrimWhitespace: IObservable; + protected readonly _stateObs = observableValue(this, WorkingSetEntryState.Modified); + readonly state: IObservable = this._stateObs; + protected readonly _isCurrentlyBeingModifiedByObs = observableValue(this, undefined); + readonly isCurrentlyBeingModifiedBy: IObservable = this._isCurrentlyBeingModifiedByObs; + private readonly initialContent: string; + + constructor( + public readonly notebookUri: URI, + public readonly cell: NotebookCellTextModel, + private readonly modifiedModel: ITextModel, + private readonly originalModel: ITextModel, + disposables: DisposableStore, + @IConfigurationService configService: IConfigurationService, + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + @INotebookEditorService private readonly notebookEditorService: INotebookEditorService + ) { + super(); + this.initialContent = this.originalModel.getValue(); + this._register(disposables); + this._register(this.modifiedModel.onDidChangeContent(e => { + this._mirrorEdits(e); + })); + this._register(toDisposable(() => { + this.clearCurrentEditLineDecoration(); + })); + + this._diffTrimWhitespace = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, configService); + this._register(autorun(r => { + this._diffTrimWhitespace.read(r); + this._updateDiffInfoSeq(); + })); + } + + public clearCurrentEditLineDecoration() { + if (this.modifiedModel.isDisposed()) { + return; + } + this._editDecorations = this.modifiedModel.deltaDecorations(this._editDecorations, []); + } + + + private _mirrorEdits(event: IModelContentChangedEvent) { + const edit = OffsetEdits.fromContentChanges(event.changes); + + if (this._isEditFromUs) { + const e_sum = this._edit; + const e_ai = edit; + this._edit = e_sum.compose(e_ai); + + } else { + + // e_ai + // d0 ---------------> s0 + // | | + // | | + // | e_user_r | e_user + // | | + // | | + // v e_ai_r v + /// d1 ---------------> s1 + // + // d0 - document snapshot + // s0 - document + // e_ai - ai edits + // e_user - user edits + // + const e_ai = this._edit; + const e_user = edit; + + const e_user_r = e_user.tryRebase(e_ai.inverse(this.originalModel.getValue()), true); + + if (e_user_r === undefined) { + // user edits overlaps/conflicts with AI edits + this._edit = e_ai.compose(e_user); + } else { + const edits = OffsetEdits.asEditOperations(e_user_r, this.originalModel); + this.originalModel.applyEdits(edits); + this._edit = e_ai.tryRebase(e_user_r); + } + + this._allEditsAreFromUs = false; + this._updateDiffInfoSeq(); + + const didResetToOriginalContent = this.modifiedModel.getValue() === this.initialContent; + const currentState = this._stateObs.get(); + switch (currentState) { + case WorkingSetEntryState.Modified: + if (didResetToOriginalContent) { + this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + break; + } + } + + } + } + + acceptAgentEdits(textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void { + const notebookEditor = this.notebookEditorService.retrieveExistingWidgetFromURI(this.notebookUri)?.value; + if (notebookEditor) { + const vm = notebookEditor.getCellByHandle(this.cell.handle); + vm?.updateEditState(CellEditState.Editing, 'chatEdit'); + } + + const ops = textEdits.map(TextEdit.asEditOperation); + const undoEdits = this._applyEdits(ops); + + const maxLineNumber = undoEdits.reduce((max, op) => Math.max(max, op.range.startLineNumber), 0); + + const newDecorations: IModelDeltaDecoration[] = [ + // decorate pending edit (region) + { + options: ChatEditingNotebookCellEntry._pendingEditDecorationOptions, + range: new Range(maxLineNumber + 1, 1, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER) + } + ]; + + if (maxLineNumber > 0) { + // decorate last edit + newDecorations.push({ + options: ChatEditingNotebookCellEntry._lastEditDecorationOptions, + range: new Range(maxLineNumber, 1, maxLineNumber, Number.MAX_SAFE_INTEGER) + }); + } + + this._editDecorations = this.modifiedModel.deltaDecorations(this._editDecorations, newDecorations); + + + transaction((tx) => { + if (!isLastEdits) { + this._stateObs.set(WorkingSetEntryState.Modified, tx); + this._isCurrentlyBeingModifiedByObs.set(responseModel, tx); + this._maxModifiedLineNumber.set(maxLineNumber, tx); + + } else { + this._resetEditsState(tx); + this._updateDiffInfoSeq(); + this._maxModifiedLineNumber.set(0, tx); + this._editDecorationClear.schedule(); + } + }); + } + + scheduleEditDecorations() { + this._editDecorationClear.schedule(); + } + + protected _resetEditsState(tx: ITransaction): void { + this._isCurrentlyBeingModifiedByObs.set(undefined, tx); + this._maxModifiedLineNumber.set(0, tx); + } + + public async keep(change: DetailedLineRangeMapping): Promise { + return this._acceptHunk(change); + } + + private async _acceptHunk(change: DetailedLineRangeMapping): Promise { + this._isEditFromUs = true; + try { + if (!this._diffInfo.get().changes.includes(change)) { + // diffInfo should have model version ids and check them (instead of the caller doing that) + return false; + } + const edits: ISingleEditOperation[] = []; + for (const edit of change.innerChanges ?? []) { + const newText = this.modifiedModel.getValueInRange(edit.modifiedRange); + edits.push(EditOperation.replace(edit.originalRange, newText)); + } + this.originalModel.pushEditOperations(null, edits, _ => null); + } + finally { + this._isEditFromUs = false; + } + await this._updateDiffInfoSeq(); + if (this._diffInfo.get().identical) { + this._stateObs.set(WorkingSetEntryState.Accepted, undefined); + } + return true; + } + + public async undo(change: DetailedLineRangeMapping): Promise { + return this._rejectHunk(change); + } + + private async _rejectHunk(change: DetailedLineRangeMapping): Promise { + this._isEditFromUs = true; + try { + if (!this._diffInfo.get().changes.includes(change)) { + return false; + } + const edits: ISingleEditOperation[] = []; + for (const edit of change.innerChanges ?? []) { + const newText = this.originalModel.getValueInRange(edit.originalRange); + edits.push(EditOperation.replace(edit.modifiedRange, newText)); + } + this.modifiedModel.pushEditOperations(null, edits, _ => null); + } finally { + this._isEditFromUs = false; + } + await this._updateDiffInfoSeq(); + if (this._diffInfo.get().identical) { + this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + } + return true; + } + + private _applyEdits(edits: ISingleEditOperation[]) { + // make the actual edit + this._isEditFromUs = true; + try { + let result: ISingleEditOperation[] = []; + this.modifiedModel.pushEditOperations(null, edits, (undoEdits) => { + result = undoEdits; + return null; + }); + return result; + } finally { + this._isEditFromUs = false; + } + } + + private async _updateDiffInfoSeq() { + const myDiffOperationId = ++this._diffOperationIds; + await Promise.resolve(this._diffOperation); + if (this._diffOperationIds === myDiffOperationId) { + const thisDiffOperation = this._updateDiffInfo(); + this._diffOperation = thisDiffOperation; + await thisDiffOperation; + } + } + + private async _updateDiffInfo(): Promise { + + if (this.originalModel.isDisposed() || this.modifiedModel.isDisposed()) { + return; + } + + const docVersionNow = this.modifiedModel.getVersionId(); + const snapshotVersionNow = this.originalModel.getVersionId(); + + const ignoreTrimWhitespace = this._diffTrimWhitespace.get(); + + const diff = await this._editorWorkerService.computeDiff( + this.originalModel.uri, + this.modifiedModel.uri, + { ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 }, + 'advanced' + ); + + if (this.originalModel.isDisposed() || this.modifiedModel.isDisposed()) { + return; + } + + // only update the diff if the documents didn't change in the meantime + if (this.modifiedModel.getVersionId() === docVersionNow && this.originalModel.getVersionId() === snapshotVersionNow) { + const diff2 = diff ?? nullDocumentDiff; + this._diffInfo.set(diff2, undefined); + this._edit = OffsetEdits.fromLineRangeMapping(this.originalModel, this.modifiedModel, diff2.changes); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts new file mode 100644 index 00000000000..bf56d104105 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts @@ -0,0 +1,616 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { autorun, derivedWithStore, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +import { debouncedObservable } from '../../../../../../base/common/observableInternal/utils.js'; +import { basename } from '../../../../../../base/common/resources.js'; +import { assertType } from '../../../../../../base/common/types.js'; +import { LineRange } from '../../../../../../editor/common/core/lineRange.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { nullDocumentDiff } from '../../../../../../editor/common/diff/documentDiffProvider.js'; +import { localize } from '../../../../../../nls.js'; +import { MenuId } from '../../../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IEditorPane, IResourceDiffEditorInput } from '../../../../../common/editor.js'; +import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { NotebookDeletedCellDecorator } from '../../../../notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.js'; +import { NotebookInsertedCellDecorator } from '../../../../notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.js'; +import { INotebookTextDiffEditor } from '../../../../notebook/browser/diff/notebookDiffEditorBrowser.js'; +import { getNotebookEditorFromEditorPane, ICellViewModel, INotebookEditor } from '../../../../notebook/browser/notebookBrowser.js'; +import { INotebookEditorService } from '../../../../notebook/browser/services/notebookEditorService.js'; +import { NotebookCellTextModel } from '../../../../notebook/common/model/notebookCellTextModel.js'; +import { NotebookTextModel } from '../../../../notebook/common/model/notebookTextModel.js'; +import { ChatAgentLocation, IChatAgentService } from '../../../common/chatAgents.js'; +import { IModifiedFileEntryChangeHunk, IModifiedFileEntryEditorIntegration } from '../../../common/chatEditingService.js'; +import { ChatEditingCodeEditorIntegration, IDocumentDiff2 } from '../chatEditingCodeEditorIntegration.js'; +import { ChatEditingModifiedNotebookEntry } from '../chatEditingModifiedNotebookEntry.js'; +import { countChanges, ICellDiffInfo, sortCellChanges } from './notebookCellChanges.js'; + +export class ChatEditingNotebookEditorIntegration extends Disposable implements IModifiedFileEntryEditorIntegration { + private integration: ChatEditingNotebookEditorWidgetIntegration; + private notebookEditor: INotebookEditor; + constructor( + _entry: ChatEditingModifiedNotebookEntry, + editor: IEditorPane, + notebookModel: NotebookTextModel, + originalModel: NotebookTextModel, + cellChanges: IObservable, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + const notebookEditor = getNotebookEditorFromEditorPane(editor); + assertType(notebookEditor); + this.notebookEditor = notebookEditor; + this.integration = this.instantiationService.createInstance(ChatEditingNotebookEditorWidgetIntegration, _entry, notebookEditor, notebookModel, originalModel, cellChanges); + this._register(editor.onDidChangeControl(() => { + const notebookEditor = getNotebookEditorFromEditorPane(editor); + if (notebookEditor && notebookEditor !== this.notebookEditor) { + this.notebookEditor = notebookEditor; + this.integration.dispose(); + this.integration = this.instantiationService.createInstance(ChatEditingNotebookEditorWidgetIntegration, _entry, notebookEditor, notebookModel, originalModel, cellChanges); + } + })); + } + public get currentIndex(): IObservable { + return this.integration.currentIndex; + } + reveal(firstOrLast: boolean): void { + return this.integration.reveal(firstOrLast); + } + next(wrap: boolean): boolean { + return this.integration.next(wrap); + } + previous(wrap: boolean): boolean { + return this.integration.previous(wrap); + } + enableAccessibleDiffView(): void { + this.integration.enableAccessibleDiffView(); + } + acceptNearestChange(change: IModifiedFileEntryChangeHunk): void { + this.integration.acceptNearestChange(change); + } + rejectNearestChange(change: IModifiedFileEntryChangeHunk): void { + this.integration.rejectNearestChange(change); + } + toggleDiff(change: IModifiedFileEntryChangeHunk | undefined): Promise { + return this.integration.toggleDiff(change); + } +} + +class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements IModifiedFileEntryEditorIntegration { + private readonly _currentIndex = observableValue(this, -1); + readonly currentIndex: IObservable = this._currentIndex; + + private readonly _currentChange = observableValue<{ change: ICellDiffInfo; index: number } | undefined>(this, undefined); + readonly currentChange: IObservable<{ change: ICellDiffInfo; index: number } | undefined> = this._currentChange; + + private readonly cellEditorIntegrations = new Map }>(); + + private readonly insertDeleteDecorators: IObservable<{ insertedCellDecorator: NotebookInsertedCellDecorator; deletedCellDecorator: NotebookDeletedCellDecorator } | undefined>; + + constructor( + private readonly _entry: ChatEditingModifiedNotebookEntry, + private readonly notebookEditor: INotebookEditor, + private readonly notebookModel: NotebookTextModel, + originalModel: NotebookTextModel, + private readonly cellChanges: IObservable, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IEditorService private readonly _editorService: IEditorService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @INotebookEditorService notebookEditorService: INotebookEditorService, + ) { + super(); + + const onDidChangeVisibleRanges = debouncedObservable(observableFromEvent(notebookEditor.onDidChangeVisibleRanges, () => notebookEditor.visibleRanges), 50); + const notebookEdotirViewModelAttached = observableFromEvent(notebookEditor.onDidAttachViewModel, () => notebookEditor.getViewModel()); + + let originalReadonly: boolean | undefined = undefined; + const shouldBeReadonly = _entry.isCurrentlyBeingModifiedBy.map(value => !!value); + this._register(autorun(r => { + const isReadOnly = shouldBeReadonly.read(r); + const notebookEditor = notebookEditorService.retrieveExistingWidgetFromURI(_entry.modifiedURI)?.value; + if (!notebookEditor) { + return; + } + originalReadonly ??= notebookEditor.isReadOnly; + if (isReadOnly) { + if (!notebookEditor.isReadOnly) { + notebookEditor.setOptions({ isReadOnly: true }); + } + } else { + if (notebookEditor.isReadOnly && originalReadonly === false) { + notebookEditor.setOptions({ isReadOnly: false }); + } + } + })); + + // INIT when not streaming nor diffing the response anymore, once per request, and when having changes + let lastModifyingRequestId: string | undefined; + this._store.add(autorun(r => { + + if (!_entry.isCurrentlyBeingModifiedBy.read(r) + && !_entry.isProcessingResponse.read(r) + && lastModifyingRequestId !== _entry.lastModifyingRequestId + && cellChanges.read(r).some(c => c.type !== 'unchanged' && !c.diff.read(r).identical) + ) { + this.reveal(true); + } + })); + + // Build cell integrations (responsible for navigating changes within a cell and decorating cell text changes) + this._register(autorun(r => { + const sortedCellChanges = sortCellChanges(cellChanges.read(r)); + + const changes = sortedCellChanges.filter(c => c.type !== 'delete'); + onDidChangeVisibleRanges.read(r); + if (!changes.length) { + this.cellEditorIntegrations.forEach(({ diff }) => { + diff.set({ ...diff.get(), ...nullDocumentDiff }, undefined); + }); + return; + } + + const validCells = new Set(); + changes.forEach((change) => { + if (change.modifiedCellIndex === undefined || change.modifiedCellIndex >= notebookModel.cells.length) { + return; + } + const cell = notebookModel.cells[change.modifiedCellIndex]; + const editor = notebookEditor.codeEditors.find(([vm,]) => vm.handle === notebookModel.cells[change.modifiedCellIndex].handle)?.[1]; + const modifiedModel = change.modifiedModel.promiseResult.read(r)?.data; + const originalModel = change.originalModel.promiseResult.read(r)?.data; + if (!editor || !cell || !originalModel || !modifiedModel) { + return; + } + const diff = { + ...change.diff.read(r), + modifiedModel, + originalModel, + keep: change.keep, + undo: change.undo + } satisfies IDocumentDiff2; + validCells.add(cell); + const currentDiff = this.cellEditorIntegrations.get(cell); + if (currentDiff) { + // Do not unnecessarily trigger a change event + if (!areDocumentDiff2Equal(currentDiff.diff.get(), diff)) { + currentDiff.diff.set(diff, undefined); + } + } else { + const diff2 = observableValue(`diff${cell.handle}`, diff); + const integration = this.instantiationService.createInstance(ChatEditingCodeEditorIntegration, _entry, editor, diff2); + this.cellEditorIntegrations.set(cell, { integration, diff: diff2 }); + this._register(integration); + this._register(editor.onDidDispose(() => { + this.cellEditorIntegrations.get(cell)?.integration.dispose(); + this.cellEditorIntegrations.delete(cell); + })); + this._register(editor.onDidChangeModel(() => { + if (editor.getModel() !== cell.textModel) { + this.cellEditorIntegrations.get(cell)?.integration.dispose(); + this.cellEditorIntegrations.delete(cell); + } + })); + } + }); + + // Dispose old integrations as the editors are no longer valid. + this.cellEditorIntegrations.forEach((v, cell) => { + if (!validCells.has(cell)) { + v.integration.dispose(); + this.cellEditorIntegrations.delete(cell); + } + }); + })); + + this._register(autorun(r => { + const currentChange = this.currentChange.read(r); + if (!currentChange) { + this._currentIndex.set(-1, undefined); + return; + } + + let index = 0; + const sortedCellChanges = sortCellChanges(cellChanges.read(r)); + for (const change of sortedCellChanges) { + if (currentChange && currentChange.change === change) { + if (change.type === 'modified') { + index += currentChange.index; + } + break; + } + if (change.type === 'insert' || change.type === 'delete') { + index++; + } else if (change.type === 'modified') { + index += change.diff.read(r).changes.length; + } + } + + this._currentIndex.set(index, undefined); + })); + + this.insertDeleteDecorators = derivedWithStore((r, store) => { + if (!notebookEdotirViewModelAttached.read(r)) { + return; + } + + const insertedCellDecorator = store.add(this.instantiationService.createInstance(NotebookInsertedCellDecorator, this.notebookEditor)); + const deletedCellDecorator = store.add(this.instantiationService.createInstance(NotebookDeletedCellDecorator, this.notebookEditor, { + className: 'chat-diff-change-content-widget', + telemetrySource: 'chatEditingNotebookHunk', + menuId: MenuId.ChatEditingEditorHunk, + argFactory: (deletedCellIndex: number) => { + return { + accept() { + const entry = cellChanges.get().find(c => c.type === 'delete' && c.originalCellIndex === deletedCellIndex); + if (entry) { + return entry.keep(entry.diff.get().changes[0]); + } + return Promise.resolve(true); + }, + reject() { + const entry = cellChanges.get().find(c => c.type === 'delete' && c.originalCellIndex === deletedCellIndex); + if (entry) { + return entry.undo(entry.diff.get().changes[0]); + } + return Promise.resolve(true); + }, + } satisfies IModifiedFileEntryChangeHunk; + } + })); + + return { + insertedCellDecorator, + deletedCellDecorator + }; + }); + + const cellsAreVisible = onDidChangeVisibleRanges.map(v => v.length > 0); + this._register(autorun(r => { + if (!cellsAreVisible.read(r)) { + return; + } + // We can have inserted cells that have been accepted, in those cases we do not wany any decorators on them. + const changes = debouncedObservable(cellChanges, 10).read(r).filter(c => c.type === 'insert' ? !c.diff.read(r).identical : true); + const decorators = debouncedObservable(this.insertDeleteDecorators, 10).read(r); + if (decorators) { + decorators.insertedCellDecorator.apply(changes); + decorators.deletedCellDecorator.apply(changes, originalModel); + } + })); + } + + getCell(modifiedCellIndex: number) { + const cell = this.notebookModel.cells[modifiedCellIndex]; + const integration = this.cellEditorIntegrations.get(cell)?.integration; + return integration; + } + + reveal(firstOrLast: boolean): void { + const changes = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged')); + if (!changes.length) { + return undefined; + } + const change = firstOrLast ? changes[0] : changes[changes.length - 1]; + this._revealFirstOrLast(change, firstOrLast); + } + + private _revealFirstOrLast(change: ICellDiffInfo, firstOrLast: boolean = true) { + switch (change.type) { + case 'insert': + case 'modified': + { + const index = firstOrLast || change.type === 'insert' ? 0 : change.diff.get().changes.length - 1; + const cellIntegration = this.getCell(change.modifiedCellIndex); + if (cellIntegration) { + cellIntegration.reveal(firstOrLast); + this._currentChange.set({ change: change, index }, undefined); + return true; + } else { + return this._revealChange(change, index); + } + } + case 'delete': + // reveal the deleted cell decorator + this.insertDeleteDecorators.get()?.deletedCellDecorator.reveal(change.originalCellIndex); + this._currentChange.set({ change: change, index: 0 }, undefined); + return true; + default: + break; + } + + return false; + } + + private _revealChange(change: ICellDiffInfo, indexInCell: number) { + switch (change.type) { + case 'insert': + case 'modified': + { + const textChange = change.diff.get().changes[indexInCell]; + const cellViewModel = this.getCellViewModel(change); + if (cellViewModel) { + this.revealChangeInView(cellViewModel, textChange.modified); + this._currentChange.set({ change: change, index: indexInCell }, undefined); + } + + return true; + } + case 'delete': + // reveal the deleted cell decorator + this.insertDeleteDecorators.get()?.deletedCellDecorator.reveal(change.originalCellIndex); + this._currentChange.set({ change: change, index: 0 }, undefined); + return true; + default: + break; + } + + return false; + } + + private getCellViewModel(change: ICellDiffInfo) { + if (change.type === 'delete' || change.modifiedCellIndex === undefined) { + return undefined; + } + const cell = this.notebookModel.cells[change.modifiedCellIndex]; + const cellViewModel = this.notebookEditor.getViewModel()?.viewCells.find(c => c.handle === cell.handle); + return cellViewModel; + } + + private async revealChangeInView(cell: ICellViewModel, lines: LineRange): Promise { + await this.notebookEditor.focusNotebookCell(cell, 'editor', { focusEditorLine: lines.startLineNumber }); + await this.notebookEditor.revealRangeInCenterAsync(cell, new Range(lines.startLineNumber, 0, lines.endLineNumberExclusive, 0)); + } + + next(wrap: boolean): boolean { + const changes = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged')); + const currentChange = this.currentChange.get(); + if (!currentChange) { + const firstChange = changes[0]; + + if (firstChange) { + return this._revealFirstOrLast(firstChange); + } + + return false; + } + + // go to next + // first check if we are at the end of the current change + switch (currentChange.change.type) { + case 'modified': + { + const cellIntegration = this.getCell(currentChange.change.modifiedCellIndex); + if (cellIntegration) { + if (cellIntegration.next(false)) { + this._currentChange.set({ change: currentChange.change, index: cellIntegration.currentIndex.get() }, undefined); + return true; + } + } + + const isLastChangeInCell = currentChange.index === lastChangeIndex(currentChange.change); + const index = isLastChangeInCell ? 0 : currentChange.index + 1; + const change = isLastChangeInCell ? changes[changes.indexOf(currentChange.change) + 1] : currentChange.change; + + if (change) { + return this._revealChange(change, index); + } + } + break; + case 'insert': + case 'delete': + { + // go to next change directly + const nextChange = changes[changes.indexOf(currentChange.change) + 1]; + if (nextChange) { + return this._revealFirstOrLast(nextChange, true); + } + } + break; + default: + break; + } + + if (wrap) { + return this.next(false); + } + + return false; + } + + previous(wrap: boolean): boolean { + const changes = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged')); + const currentChange = this.currentChange.get(); + if (!currentChange) { + const lastChange = changes[changes.length - 1]; + if (lastChange) { + return this._revealFirstOrLast(lastChange, false); + } + + return false; + } + + // go to previous + // first check if we are at the start of the current change + switch (currentChange.change.type) { + case 'modified': + { + const cellIntegration = this.getCell(currentChange.change.modifiedCellIndex); + if (cellIntegration) { + if (cellIntegration.previous(false)) { + this._currentChange.set({ change: currentChange.change, index: cellIntegration.currentIndex.get() }, undefined); + return true; + } + } + + const isFirstChangeInCell = currentChange.index === 0; + const index = isFirstChangeInCell ? 0 : currentChange.index - 1; + const change = isFirstChangeInCell ? changes[changes.indexOf(currentChange.change) - 1] : currentChange.change; + + if (change) { + return this._revealChange(change, index); + } + } + break; + case 'insert': + case 'delete': + { + // go to previous change directly + const prevChange = changes[changes.indexOf(currentChange.change) - 1]; + if (prevChange) { + return this._revealFirstOrLast(prevChange, false); + } + } + break; + default: + break; + } + + if (wrap) { + const lastChange = changes[changes.length - 1]; + if (lastChange) { + return this._revealFirstOrLast(lastChange, false); + } + } + + return false; + } + + enableAccessibleDiffView(): void { + const cell = this.notebookEditor.getActiveCell()?.model; + if (cell) { + const integration = this.cellEditorIntegrations.get(cell)?.integration; + integration?.enableAccessibleDiffView(); + } + } + acceptNearestChange(change: IModifiedFileEntryChangeHunk): void { + change.accept(); + this.next(true); + } + rejectNearestChange(change: IModifiedFileEntryChangeHunk): void { + change.reject(); + this.next(true); + } + async toggleDiff(_change: IModifiedFileEntryChangeHunk | undefined): Promise { + const defaultAgentName = this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName; + const diffInput = { + original: { resource: this._entry.originalURI, options: { selection: undefined } }, + modified: { resource: this._entry.modifiedURI, options: { selection: undefined } }, + label: defaultAgentName + ? localize('diff.agent', '{0} (changes from {1})', basename(this._entry.modifiedURI), defaultAgentName) + : localize('diff.generic', '{0} (changes from chat)', basename(this._entry.modifiedURI)) + } satisfies IResourceDiffEditorInput; + await this._editorService.openEditor(diffInput); + + } +} + +export class ChatEditingNotebookDiffEditorIntegration extends Disposable implements IModifiedFileEntryEditorIntegration { + private readonly _currentIndex = observableValue(this, -1); + readonly currentIndex: IObservable = this._currentIndex; + + constructor( + private readonly notebookDiffEditor: INotebookTextDiffEditor, + private readonly cellChanges: IObservable + ) { + super(); + + this._store.add(autorun(r => { + const index = notebookDiffEditor.currentChangedIndex.read(r); + const numberOfCellChanges = cellChanges.read(r).filter(c => !c.diff.read(r).identical); + if (numberOfCellChanges.length && index >= 0 && index < numberOfCellChanges.length) { + // Notebook Diff editor only supports navigating through changes to cells. + // However in chat we take changes to lines in the cells into account. + // So if we're on the second cell and first cell has 3 changes, then we're on the 4th change. + const changesSoFar = countChanges(numberOfCellChanges.slice(0, index + 1)); + this._currentIndex.set(changesSoFar - 1, undefined); + } else { + this._currentIndex.set(-1, undefined); + } + })); + } + + reveal(firstOrLast: boolean): void { + const changes = sortCellChanges(this.cellChanges.get().filter(c => c.type !== 'unchanged')); + if (!changes.length) { + return undefined; + } + if (firstOrLast) { + this.notebookDiffEditor.firstChange(); + } else { + this.notebookDiffEditor.lastChange(); + } + } + + next(_wrap: boolean): boolean { + const changes = this.cellChanges.get().filter(c => !c.diff.get().identical).length; + if (this.notebookDiffEditor.currentChangedIndex.get() === changes - 1) { + return false; + } + this.notebookDiffEditor.nextChange(); + return true; + } + + previous(_wrap: boolean): boolean { + const changes = this.cellChanges.get().filter(c => !c.diff.get().identical).length; + if (this.notebookDiffEditor.currentChangedIndex.get() === changes - 1) { + return false; + } + this.notebookDiffEditor.nextChange(); + return true; + } + + enableAccessibleDiffView(): void { + // + } + acceptNearestChange(change: IModifiedFileEntryChangeHunk): void { + change.accept(); + this.next(true); + } + rejectNearestChange(change: IModifiedFileEntryChangeHunk): void { + change.reject(); + this.next(true); + } + async toggleDiff(_change: IModifiedFileEntryChangeHunk | undefined): Promise { + // + } +} + +function areDocumentDiff2Equal(diff1: IDocumentDiff2, diff2: IDocumentDiff2): boolean { + if (diff1.changes !== diff2.changes) { + return false; + } + if (diff1.identical !== diff2.identical) { + return false; + } + if (diff1.moves !== diff2.moves) { + return false; + } + if (diff1.originalModel !== diff2.originalModel) { + return false; + } + if (diff1.modifiedModel !== diff2.modifiedModel) { + return false; + } + if (diff1.keep !== diff2.keep) { + return false; + } + if (diff1.undo !== diff2.undo) { + return false; + } + if (diff1.quitEarly !== diff2.quitEarly) { + return false; + } + return true; +} + +function lastChangeIndex(change: ICellDiffInfo): number { + if (change.type === 'modified') { + return change.diff.get().changes.length - 1; + } + return 0; +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/chatEditingNotebookFileSystemProvider.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookFileSystemProvider.ts similarity index 64% rename from src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/chatEditingNotebookFileSystemProvider.ts rename to src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookFileSystemProvider.ts index 0b3d8cd343d..b79dd102d43 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/chatEditingNotebookFileSystemProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookFileSystemProvider.ts @@ -10,46 +10,32 @@ import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle import { ResourceMap } from '../../../../../../base/common/map.js'; import { ReadableStreamEvents } from '../../../../../../base/common/stream.js'; import { URI } from '../../../../../../base/common/uri.js'; -import { IFileService, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, IFileDeleteOptions, IFileOverwriteOptions, IFileWriteOptions, IFileReadStreamOptions, IFileOpenOptions, FileType } from '../../../../../../platform/files/common/files.js'; +import { FileSystemProviderCapabilities, FileType, IFileChange, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, IFileService, IFileSystemProvider, IFileWriteOptions, IStat, IWatchOptions } from '../../../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution } from '../../../../../common/contributions.js'; +import { INotebookService } from '../../../../notebook/common/notebookService.js'; +import { IChatEditingService } from '../../../common/chatEditingService.js'; +import { ChatEditingNotebookSnapshotScheme, deserializeSnapshot } from './chatEditingModifiedNotebookSnapshot.js'; +import { ChatEditingSession } from '../chatEditingSession.js'; export class ChatEditingNotebookFileSystemProviderContrib extends Disposable implements IWorkbenchContribution { static ID = 'chatEditingNotebookFileSystemProviderContribution'; constructor( - @IFileService private readonly fileService: IFileService) { + @IFileService private readonly fileService: IFileService, + @IInstantiationService instantiationService: IInstantiationService, + ) { super(); - this._register(this.fileService.registerProvider(ChatEditingNotebookFileSystemProvider.scheme, new ChatEditingNotebookFileSystemProvider())); + const fileSystemProvider = instantiationService.createInstance(ChatEditingNotebookFileSystemProvider); + this._register(this.fileService.registerProvider(ChatEditingNotebookSnapshotScheme, fileSystemProvider)); } } +type ChatEditingSnapshotNotebookContentQueryData = { sessionId: string; requestId: string | undefined; undoStop: string | undefined; viewType: string }; + export class ChatEditingNotebookFileSystemProvider implements IFileSystemProvider { - public static readonly scheme = 'chat-editing-notebook-model'; private static registeredFiles = new ResourceMap(); - - public static getEmptyFileURI(): URI { - return URI.from({ - scheme: ChatEditingNotebookFileSystemProvider.scheme, - query: JSON.stringify({ kind: 'empty' }), - }); - } - - public static getFileURI(documentId: string, path: string): URI { - return URI.from({ - scheme: ChatEditingNotebookFileSystemProvider.scheme, - path, - query: JSON.stringify({ kind: 'doc' }), - }); - } - public static getSnapshotFileURI(requestId: string | undefined, path: string): URI { - return URI.from({ - scheme: ChatEditingNotebookFileSystemProvider.scheme, - path, - query: JSON.stringify({ requestId: requestId ?? '' }), - }); - } - public readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly | FileSystemProviderCapabilities.FileAtomicRead | FileSystemProviderCapabilities.FileReadWrite; public static registerFile(resource: URI, buffer: VSBuffer): IDisposable { ChatEditingNotebookFileSystemProvider.registeredFiles.set(resource, buffer); @@ -62,6 +48,9 @@ export class ChatEditingNotebookFileSystemProvider implements IFileSystemProvide }; } + constructor( + @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @INotebookService private readonly notebookService: INotebookService) { } readonly onDidChangeCapabilities = Event.None; readonly onDidChangeFile: Event = Event.None; watch(_resource: URI, _opts: IWatchOptions): IDisposable { @@ -92,11 +81,27 @@ export class ChatEditingNotebookFileSystemProvider implements IFileSystemProvide } async readFile(resource: URI): Promise { const buffer = ChatEditingNotebookFileSystemProvider.registeredFiles.get(resource); - if (!buffer) { - throw new Error('File not found'); + if (buffer) { + return buffer.buffer; } - return buffer.buffer; + const queryData = JSON.parse(resource.query) as ChatEditingSnapshotNotebookContentQueryData; + if (!queryData.viewType) { + throw new Error('File not found, viewType not found'); + } + const session = this._chatEditingService.getEditingSession(queryData.sessionId); + if (!(session instanceof ChatEditingSession) || !queryData.requestId) { + throw new Error('File not found, session not found'); + } + const snapshotEntry = session.getSnapshot(queryData.requestId, queryData.undoStop || undefined, resource); + if (!snapshotEntry) { + throw new Error('File not found, snapshot not found'); + } + + const { data } = deserializeSnapshot(snapshotEntry.current); + const { serializer } = await this.notebookService.withNotebookDataProvider(queryData.viewType); + return serializer.notebookToData(data).then(s => s.buffer); } + writeFile?(__resource: URI, _content: Uint8Array, _opts: IFileWriteOptions): Promise { throw new Error('Method not implemented7.'); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/helpers.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/helpers.ts new file mode 100644 index 00000000000..23a647019cb --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/helpers.ts @@ -0,0 +1,405 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotebookTextModel } from '../../../../notebook/common/model/notebookTextModel.js'; +import { CellEditType, ICell, ICellDto2, ICellEditOperation, ICellReplaceEdit, NotebookCellsChangeType, NotebookCellsModelMoveEvent, NotebookCellTextModelSplice, NotebookTextModelChangedEvent } from '../../../../notebook/common/notebookCommon.js'; +import { ICellDiffInfo, sortCellChanges } from './notebookCellChanges.js'; + + +export function adjustCellDiffForKeepingADeletedCell(originalCellIndex: number, + cellDiffInfo: ICellDiffInfo[], + applyEdits: typeof NotebookTextModel.prototype.applyEdits, +): ICellDiffInfo[] { + // Delete this cell from original as well. + const edit: ICellReplaceEdit = { cells: [], count: 1, editType: CellEditType.Replace, index: originalCellIndex, }; + applyEdits([edit], true, undefined, () => undefined, undefined, true); + const diffs = sortCellChanges(cellDiffInfo) + .filter(d => !(d.type === 'delete' && d.originalCellIndex === originalCellIndex)) + .map(diff => { + if (diff.type !== 'insert' && diff.originalCellIndex > originalCellIndex) { + return { + ...diff, + originalCellIndex: diff.originalCellIndex - 1, + }; + } + return diff; + }); + return diffs; +} + +export function adjustCellDiffForRevertingADeletedCell(originalCellIndex: number, + cellDiffInfo: ICellDiffInfo[], + cellToInsert: ICellDto2, + applyEdits: typeof NotebookTextModel.prototype.applyEdits, + createModifiedCellDiffInfo: (modifiedCellIndex: number, originalCellIndex: number) => ICellDiffInfo, +): ICellDiffInfo[] { + cellDiffInfo = sortCellChanges(cellDiffInfo); + const indexOfEntry = cellDiffInfo.findIndex(d => d.originalCellIndex === originalCellIndex); + if (indexOfEntry === -1) { + // Not possible. + return cellDiffInfo; + } + + let modifiedCellIndex = -1; + for (let i = 0; i < cellDiffInfo.length; i++) { + const diff = cellDiffInfo[i]; + if (i < indexOfEntry) { + modifiedCellIndex = Math.max(modifiedCellIndex, diff.modifiedCellIndex ?? modifiedCellIndex); + continue; + } + if (i === indexOfEntry) { + const edit: ICellReplaceEdit = { cells: [cellToInsert], count: 0, editType: CellEditType.Replace, index: modifiedCellIndex + 1, }; + applyEdits([edit], true, undefined, () => undefined, undefined, true); + cellDiffInfo[i] = createModifiedCellDiffInfo(modifiedCellIndex + 1, originalCellIndex); + continue; + } else { + // Increase the original index for all entries after this. + if (typeof diff.modifiedCellIndex === 'number') { + diff.modifiedCellIndex++; + cellDiffInfo[i] = { ...diff }; + } + } + } + + return cellDiffInfo; +} + +export function adjustCellDiffForRevertingAnInsertedCell(modifiedCellIndex: number, + cellDiffInfo: ICellDiffInfo[], + applyEdits: typeof NotebookTextModel.prototype.applyEdits, +): ICellDiffInfo[] { + if (modifiedCellIndex === -1) { + // Not possible. + return cellDiffInfo; + } + cellDiffInfo = sortCellChanges(cellDiffInfo) + .filter(d => !(d.type === 'insert' && d.modifiedCellIndex === modifiedCellIndex)) + .map(d => { + if (d.type === 'insert' && d.modifiedCellIndex === modifiedCellIndex) { + return d; + } + if (d.type !== 'delete' && d.modifiedCellIndex > modifiedCellIndex) { + return { + ...d, + modifiedCellIndex: d.modifiedCellIndex - 1, + }; + } + return d; + }); + const edit: ICellReplaceEdit = { cells: [], count: 1, editType: CellEditType.Replace, index: modifiedCellIndex, }; + applyEdits([edit], true, undefined, () => undefined, undefined, true); + return cellDiffInfo; +} + +export function adjustCellDiffForKeepingAnInsertedCell(modifiedCellIndex: number, + cellDiffInfo: ICellDiffInfo[], + cellToInsert: ICellDto2, + applyEdits: typeof NotebookTextModel.prototype.applyEdits, + createModifiedCellDiffInfo: (modifiedCellIndex: number, originalCellIndex: number) => ICellDiffInfo, +): ICellDiffInfo[] { + cellDiffInfo = sortCellChanges(cellDiffInfo); + if (modifiedCellIndex === -1) { + // Not possible. + return cellDiffInfo; + } + const indexOfEntry = cellDiffInfo.findIndex(d => d.modifiedCellIndex === modifiedCellIndex); + if (indexOfEntry === -1) { + // Not possible. + return cellDiffInfo; + } + let originalCellIndex = -1; + for (let i = 0; i < cellDiffInfo.length; i++) { + const diff = cellDiffInfo[i]; + if (i < indexOfEntry) { + originalCellIndex = Math.max(originalCellIndex, diff.originalCellIndex ?? originalCellIndex); + continue; + } + if (i === indexOfEntry) { + const edit: ICellReplaceEdit = { cells: [cellToInsert], count: 0, editType: CellEditType.Replace, index: originalCellIndex + 1 }; + applyEdits([edit], true, undefined, () => undefined, undefined, true); + cellDiffInfo[i] = createModifiedCellDiffInfo(modifiedCellIndex, originalCellIndex + 1); + continue; + } else { + // Increase the original index for all entries after this. + if (typeof diff.originalCellIndex === 'number') { + diff.originalCellIndex++; + cellDiffInfo[i] = { ...diff }; + } + } + } + return cellDiffInfo; +} + +export function adjustCellDiffAndOriginalModelBasedOnCellAddDelete(change: NotebookCellTextModelSplice, + cellDiffInfo: ICellDiffInfo[], + modifiedModelCellCount: number, + originalModelCellCount: number, + applyEdits: typeof NotebookTextModel.prototype.applyEdits, + createModifiedCellDiffInfo: (modifiedCellIndex: number, originalCellIndex: number) => ICellDiffInfo, +): ICellDiffInfo[] { + cellDiffInfo = sortCellChanges(cellDiffInfo); + const numberOfCellsInserted = change[2].length; + const numberOfCellsDeleted = change[1]; + const cells = change[2].map(cell => { + return { + cellKind: cell.cellKind, + language: cell.language, + metadata: cell.metadata, + outputs: cell.outputs, + source: cell.getValue(), + mime: undefined, + internalMetadata: cell.internalMetadata + } satisfies ICellDto2; + }); + const wasInsertedAsFirstCell = change[0] === 0; + const wasInsertedAsLastCell = change[0] === modifiedModelCellCount - 1; + const diffEntryIndex = wasInsertedAsFirstCell ? 0 : (wasInsertedAsLastCell ? cellDiffInfo.length - 1 : (cellDiffInfo.findIndex(d => d.modifiedCellIndex === change[0]))); + const indexToInsertInOriginalModel = (wasInsertedAsFirstCell || diffEntryIndex === -1) ? 0 : (wasInsertedAsLastCell ? originalModelCellCount : (((cellDiffInfo.slice(0, diffEntryIndex).reverse().find(c => typeof c.originalCellIndex === 'number')?.originalCellIndex ?? -1) + 1))); + if (cells.length) { + const edit: ICellEditOperation = { + editType: CellEditType.Replace, + cells, + index: indexToInsertInOriginalModel, + count: change[1] + }; + applyEdits([edit], true, undefined, () => undefined, undefined, true); + } + // If cells were deleted we handled that with this.disposeDeletedCellEntries(); + if (numberOfCellsDeleted) { + // Adjust the indexes. + let numberOfOriginalCellsRemovedSoFar = 0; + let numberOfModifiedCellsRemovedSoFar = 0; + const modifiedIndexesToRemove = new Set(); + for (let i = 0; i < numberOfCellsDeleted; i++) { + modifiedIndexesToRemove.add(change[0] + i); + } + const itemsToRemove = new Set(); + for (let i = 0; i < cellDiffInfo.length; i++) { + const diff = cellDiffInfo[i]; + if (i < diffEntryIndex) { + continue; + } + + let changed = false; + if (typeof diff.modifiedCellIndex === 'number' && modifiedIndexesToRemove.has(diff.modifiedCellIndex)) { + // This will be removed. + numberOfModifiedCellsRemovedSoFar++; + if (typeof diff.originalCellIndex === 'number') { + numberOfOriginalCellsRemovedSoFar++; + } + itemsToRemove.add(diff); + continue; + } + if (typeof diff.modifiedCellIndex === 'number' && numberOfModifiedCellsRemovedSoFar) { + diff.modifiedCellIndex -= numberOfModifiedCellsRemovedSoFar; + changed = true; + } + if (typeof diff.originalCellIndex === 'number' && numberOfOriginalCellsRemovedSoFar) { + diff.originalCellIndex -= numberOfOriginalCellsRemovedSoFar; + changed = true; + } + if (changed) { + cellDiffInfo[i] = { ...diff }; + } + } + if (itemsToRemove.size) { + Array.from(itemsToRemove) + .filter(diff => typeof diff.originalCellIndex === 'number') + .forEach(diff => { + const edit: ICellEditOperation = { + editType: CellEditType.Replace, + cells: [], + index: diff.originalCellIndex, + count: 1 + }; + applyEdits([edit], true, undefined, () => undefined, undefined, true); + }); + } + cellDiffInfo = cellDiffInfo.filter(d => !itemsToRemove.has(d)); + } + + if (numberOfCellsInserted) { + for (let i = 0; i < cellDiffInfo.length; i++) { + const diff = cellDiffInfo[i]; + if (i < diffEntryIndex) { + continue; + } + let changed = false; + if (typeof diff.modifiedCellIndex === 'number') { + diff.modifiedCellIndex += numberOfCellsInserted; + changed = true; + } + if (typeof diff.originalCellIndex === 'number') { + diff.originalCellIndex += numberOfCellsInserted; + changed = true; + } + if (changed) { + cellDiffInfo[i] = { ...diff }; + } + } + } + + // For inserted cells, we need to ensure that we create a corresponding CellEntry. + // So that any edits to the inserted cell is handled and mirrored over to the corresponding cell in original model. + cells.forEach((_, i) => { + const originalCellIndex = i + indexToInsertInOriginalModel; + const modifiedCellIndex = change[0] + i; + const unchangedCell = createModifiedCellDiffInfo(modifiedCellIndex, originalCellIndex); + cellDiffInfo.splice((diffEntryIndex === -1 ? 0 : diffEntryIndex) + i, 0, unchangedCell); + }); + return cellDiffInfo; +} + +/** + * Given the movements of cells in modified notebook, adjust the ICellDiffInfo[] array + * and generate edits for the old notebook (if required). + * TODO@DonJayamanne Handle bulk moves (movements of more than 1 cell). + */ +export function adjustCellDiffAndOriginalModelBasedOnCellMovements(event: NotebookCellsModelMoveEvent, cellDiffInfo: ICellDiffInfo[]): [ICellDiffInfo[], ICellEditOperation[]] | undefined { + const minimumIndex = Math.min(event.index, event.newIdx); + const maximumIndex = Math.max(event.index, event.newIdx); + const cellDiffs = cellDiffInfo.slice(); + const indexOfEntry = cellDiffs.findIndex(d => d.modifiedCellIndex === event.index); + const indexOfEntryToPlaceBelow = cellDiffs.findIndex(d => d.modifiedCellIndex === event.newIdx); + if (indexOfEntry === -1 || indexOfEntryToPlaceBelow === -1) { + return undefined; + } + // Create a new object so that the observable value is triggered. + // Besides we'll be updating the values of this object in place. + const entryToBeMoved = { ...cellDiffs[indexOfEntry] }; + const moveDirection = event.newIdx > event.index ? 'down' : 'up'; + + + const startIndex = cellDiffs.findIndex(d => d.modifiedCellIndex === minimumIndex); + const endIndex = cellDiffs.findIndex(d => d.modifiedCellIndex === maximumIndex); + const movingExistingCell = typeof entryToBeMoved.originalCellIndex === 'number'; + let originalCellsWereEffected = false; + for (let i = 0; i < cellDiffs.length; i++) { + const diff = cellDiffs[i]; + let changed = false; + if (moveDirection === 'down') { + if (i > startIndex && i <= endIndex) { + if (typeof diff.modifiedCellIndex === 'number') { + changed = true; + diff.modifiedCellIndex = diff.modifiedCellIndex - 1; + } + if (typeof diff.originalCellIndex === 'number' && movingExistingCell) { + diff.originalCellIndex = diff.originalCellIndex - 1; + originalCellsWereEffected = true; + changed = true; + } + } + } else { + if (i >= startIndex && i < endIndex) { + if (typeof diff.modifiedCellIndex === 'number') { + changed = true; + diff.modifiedCellIndex = diff.modifiedCellIndex + 1; + } + if (typeof diff.originalCellIndex === 'number' && movingExistingCell) { + diff.originalCellIndex = diff.originalCellIndex + 1; + originalCellsWereEffected = true; + changed = true; + } + } + } + // Create a new object so that the observable value is triggered. + // Do only if there's a change. + if (changed) { + cellDiffs[i] = { ...diff }; + } + } + entryToBeMoved.modifiedCellIndex = event.newIdx; + const originalCellIndex = entryToBeMoved.originalCellIndex; + if (moveDirection === 'down') { + cellDiffs.splice(endIndex + 1, 0, entryToBeMoved); + cellDiffs.splice(startIndex, 1); + // If we're moving a new cell up/down, then we need just adjust just the modified indexes of the cells in between. + // If we're moving an existing up/down, then we need to adjust the original indexes as well. + if (typeof entryToBeMoved.originalCellIndex === 'number') { + entryToBeMoved.originalCellIndex = cellDiffs.slice(0, endIndex).reduce((lastOriginalIndex, diff) => typeof diff.originalCellIndex === 'number' ? Math.max(lastOriginalIndex, diff.originalCellIndex) : lastOriginalIndex, -1) + 1; + } + } else { + cellDiffs.splice(endIndex, 1); + cellDiffs.splice(startIndex, 0, entryToBeMoved); + // If we're moving a new cell up/down, then we need just adjust just the modified indexes of the cells in between. + // If we're moving an existing up/down, then we need to adjust the original indexes as well. + if (typeof entryToBeMoved.originalCellIndex === 'number') { + entryToBeMoved.originalCellIndex = cellDiffs.slice(0, startIndex).reduce((lastOriginalIndex, diff) => typeof diff.originalCellIndex === 'number' ? Math.max(lastOriginalIndex, diff.originalCellIndex) : lastOriginalIndex, -1) + 1; + } + } + + // If this is a new cell that we're moving, and there are no existing cells in between, then we can just move the new cell. + // I.e. no need to update the original notebook model. + if (typeof entryToBeMoved.originalCellIndex === 'number' && originalCellsWereEffected && typeof originalCellIndex === 'number' && entryToBeMoved.originalCellIndex !== originalCellIndex) { + const edit: ICellEditOperation = { + editType: CellEditType.Move, + index: originalCellIndex, + length: event.length, + newIdx: entryToBeMoved.originalCellIndex + }; + + return [cellDiffs, [edit]]; + } + + return [cellDiffs, []]; +} + +export function getCorrespondingOriginalCellIndex(modifiedCellIndex: number, cellDiffInfo: ICellDiffInfo[]): number | undefined { + const entry = cellDiffInfo.find(d => d.modifiedCellIndex === modifiedCellIndex); + return entry?.originalCellIndex; +} + +/** + * + * This isn't great, but necessary. + * ipynb extension updates metadata when new cells are inserted (to ensure the metadata is correct) + * Details of why thats required is in ipynb extension, but its necessary. + * However as a result of this, those edits appear here and are assumed to be user edits. + * As a result `_allEditsAreFromUs` is set to false. + */ +export function isTransientIPyNbExtensionEvent(notebookKind: string, e: NotebookTextModelChangedEvent) { + if (notebookKind !== 'jupyter-notebook') { + return false; + } + if (e.rawEvents.every(event => { + if (event.kind !== NotebookCellsChangeType.ChangeCellMetadata) { + return false; + } + if (JSON.stringify(event.metadata || {}) === JSON.stringify({ execution_count: null, metadata: {} })) { + return true; + } + return true; + + })) { + return true; + } + + return false; +} + +export function calculateNotebookRewriteRatio(cellsDiff: ICellDiffInfo[], originalModel: NotebookTextModel, modifiedModel: NotebookTextModel): number { + const totalNumberOfUpdatedLines = cellsDiff.reduce((totalUpdatedLines, value) => { + const getUpadtedLineCount = () => { + if (value.type === 'unchanged') { + return 0; + } + if (value.type === 'delete') { + return originalModel.cells[value.originalCellIndex].textModel?.getLineCount() ?? 0; + } + if (value.type === 'insert') { + return modifiedModel.cells[value.modifiedCellIndex].textModel?.getLineCount() ?? 0; + } + return value.diff.get().changes.reduce((maxLineNumber, change) => { + return Math.max(maxLineNumber, change.modified.endLineNumberExclusive); + }, 0); + }; + + return totalUpdatedLines + getUpadtedLineCount(); + }, 0); + + const totalNumberOfLines = modifiedModel.cells.reduce((totalLines, cell) => totalLines + (cell.textModel?.getLineCount() ?? 0), 0); + return totalNumberOfLines === 0 ? 0 : Math.min(1, totalNumberOfUpdatedLines / totalNumberOfLines); + +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/notebookCellChanges.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/notebookCellChanges.ts new file mode 100644 index 00000000000..376d0ed8d5f --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/notebookCellChanges.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ISettableObservable, ObservablePromise } from '../../../../../../base/common/observable.js'; +import { IDocumentDiff } from '../../../../../../editor/common/diff/documentDiffProvider.js'; +import { DetailedLineRangeMapping } from '../../../../../../editor/common/diff/rangeMapping.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; + + +/** + * This structure is used to represent the state of a Notebook document compared to the original. + * Its similar to the IDocumentDiff object, that tells us what cells are unmodified, modified, inserted or deleted. + * + * All entries will contain a IDocumentDiff + * Even when there are no changes, diff will contain the number of lines in the document. + * This way we can always calculate the total number of lines in the document. + */ +export type ICellDiffInfo = | + { + originalCellIndex: number; + modifiedCellIndex: number; + type: 'unchanged'; + } & IDocumentDiffWithModelsAndActions | + { + originalCellIndex: number; + modifiedCellIndex: number; + type: 'modified'; + } & IDocumentDiffWithModelsAndActions | + { + modifiedCellIndex: undefined; + originalCellIndex: number; + type: 'delete'; + } & IDocumentDiffWithModelsAndActions | + { + modifiedCellIndex: number; + originalCellIndex: undefined; + type: 'insert'; + } & IDocumentDiffWithModelsAndActions; + + + +interface IDocumentDiffWithModelsAndActions { + /** + * The changes between the original and modified document. + */ + diff: ISettableObservable; + /** + * The original model. + * Cell text models load asynchronously, so this is an observable promise. + */ + originalModel: ObservablePromise; + /** + * The modified model. + * Cell text models load asynchronously, so this is an observable promise. + */ + modifiedModel: ObservablePromise; + keep(changes: DetailedLineRangeMapping): Promise; + undo(changes: DetailedLineRangeMapping): Promise; +} + + +export function countChanges(changes: ICellDiffInfo[]): number { + return changes.reduce((count, change) => { + const diff = change.diff.get(); + // When we accept some of the cell insert/delete the items might still be in the list. + if (diff.identical) { + return count; + } + switch (change.type) { + case 'delete': + return count + 1; // We want to see 1 deleted entry in the pill for navigation + case 'insert': + return count + 1; // We want to see 1 new entry in the pill for navigation + case 'modified': + return count + diff.changes.length; + default: + return count; + } + }, 0); + +} + +export function sortCellChanges(changes: ICellDiffInfo[]): ICellDiffInfo[] { + return [...changes].sort((a, b) => { + // For unchanged and modified, use modifiedCellIndex + if ((a.type === 'unchanged' || a.type === 'modified') && + (b.type === 'unchanged' || b.type === 'modified')) { + return a.modifiedCellIndex - b.modifiedCellIndex; + } + + // For delete entries, use originalCellIndex + if (a.type === 'delete' && b.type === 'delete') { + return a.originalCellIndex - b.originalCellIndex; + } + + // For insert entries, use modifiedCellIndex + if (a.type === 'insert' && b.type === 'insert') { + return a.modifiedCellIndex - b.modifiedCellIndex; + } + + if (a.type === 'delete' && b.type === 'insert') { + return -1; + } + if (a.type === 'insert' && b.type === 'delete') { + return 1; + } + + if ((a.type === 'delete' && b.type !== 'insert') || (a.type !== 'insert' && b.type === 'delete')) { + return a.originalCellIndex - b.originalCellIndex; + } + + // Mixed types: compare based on available indices + const aIndex = a.type === 'delete' ? a.originalCellIndex : + (a.type === 'insert' ? a.modifiedCellIndex : a.modifiedCellIndex); + const bIndex = b.type === 'delete' ? b.originalCellIndex : + (b.type === 'insert' ? b.modifiedCellIndex : b.modifiedCellIndex); + + return aIndex - bIndex; + }); +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 7b89c3d65a6..cccd397d87e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -19,11 +19,11 @@ import { Memento } from '../../../common/memento.js'; import { clearChatEditor } from './actions/chatClear.js'; import { ChatEditorInput } from './chatEditorInput.js'; import { ChatWidget, IChatViewState } from './chatWidget.js'; -import { ChatAgentLocation } from '../common/chatAgents.js'; import { IChatModel, IExportableChatData, ISerializableChatData } from '../common/chatModel.js'; import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js'; import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; import { EDITOR_DRAG_AND_DROP_BACKGROUND } from '../../../common/theme.js'; +import { ChatAgentLocation } from '../common/constants.js'; export interface IChatEditorOptions extends IEditorOptions { target?: { sessionId: string } | { data: IExportableChatData | ISerializableChatData }; @@ -68,6 +68,7 @@ export class ChatEditor extends EditorPane { undefined, { supportsFileReferences: true, + enableImplicitContext: true, }, { listForeground: editorForeground, diff --git a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts index d600d02a790..f4d55df96e4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts +++ b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts @@ -8,9 +8,10 @@ import { Button, IButtonStyles } from '../../../../base/browser/ui/button/button import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { localize } from '../../../../nls.js'; -import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; +import { IChatAgentService } from '../common/chatAgents.js'; import { formatChatQuestion } from '../common/chatParserTypes.js'; import { IChatFollowup } from '../common/chatService.js'; +import { ChatAgentLocation } from '../common/constants.js'; const $ = dom.$; diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts index 0e19db104f3..3752bff4c6b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -7,10 +7,8 @@ import * as dom from '../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { Lazy } from '../../../../base/common/lazy.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; @@ -92,8 +90,6 @@ export class InlineAnchorWidget extends Disposable { const contextKeyService = this._register(originalContextKeyService.createScoped(element)); this._chatResourceContext = chatAttachmentResourceContextKey.bindTo(contextKeyService); - const anchorId = new Lazy(generateUuid); - element.classList.add(InlineAnchorWidget.className, 'show-file-icons'); let iconText: string; @@ -109,18 +105,6 @@ export class InlineAnchorWidget extends Disposable { iconText = this.data.symbol.name; iconClasses = ['codicon', ...getIconClasses(modelService, languageService, undefined, undefined, SymbolKinds.toIcon(symbol.kind))]; - this._register(dom.addDisposableListener(element, 'click', () => { - telemetryService.publicLog2<{ - anchorId: string; - }, { - anchorId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for the current anchor.' }; - owner: 'mjbvz'; - comment: 'Provides insight into the usage of Chat features.'; - }>('chat.inlineAnchor.openSymbol', { - anchorId: anchorId.value - }); - })); - this._store.add(instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, element, contextKeyService, { value: symbol.location, name: symbol.name, kind: symbol.kind }, MenuId.ChatInlineSymbolAnchorContext))); } else { location = this.data; @@ -156,18 +140,6 @@ export class InlineAnchorWidget extends Disposable { }) .catch(() => { }); - this._register(dom.addDisposableListener(element, 'click', () => { - telemetryService.publicLog2<{ - anchorId: string; - }, { - anchorId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for the current anchor.' }; - owner: 'mjbvz'; - comment: 'Provides insight into the usage of Chat features.'; - }>('chat.inlineAnchor.openResource', { - anchorId: anchorId.value - }); - })); - // Context menu this._register(dom.addDisposableListener(element, dom.EventType.CONTEXT_MENU, async domEvent => { const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 94405a7465c..a4919cceeaa 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -12,19 +12,15 @@ import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser import * as aria from '../../../../base/browser/ui/aria/aria.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { IActionProvider } from '../../../../base/browser/ui/dropdown/dropdown.js'; -import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; -import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { IAction, Separator, toAction } from '../../../../base/common/actions.js'; -import { Promises } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { HistoryNavigator2 } from '../../../../base/common/history.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; -import { basename, dirname } from '../../../../base/common/path.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { URI } from '../../../../base/common/uri.js'; import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js'; @@ -33,10 +29,8 @@ import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/c import { EditorOptions } from '../../../../editor/common/config/editorOptions.js'; import { IDimension } from '../../../../editor/common/core/dimension.js'; import { IPosition } from '../../../../editor/common/core/position.js'; -import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { Range } from '../../../../editor/common/core/range.js'; import { ITextModel } from '../../../../editor/common/model.js'; -import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { CopyPasteController } from '../../../../editor/contrib/dropOrPasteInto/browser/copyPasteController.js'; @@ -57,10 +51,8 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js'; -import { FileKind, IFileService } from '../../../../platform/files/common/files.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; import { registerAndCreateHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -68,39 +60,41 @@ import { ILabelService } from '../../../../platform/label/common/label.js'; import { WorkbenchList } from '../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { IOpenerService, type OpenInternalOptions } from '../../../../platform/opener/common/opener.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { IFileLabelOptions, ResourceLabels } from '../../../browser/labels.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { ResourceLabels } from '../../../browser/labels.js'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js'; -import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; +import { IChatEditingSession } from '../common/chatEditingService.js'; +import { ChatEntitlement, IChatEntitlementService } from '../common/chatEntitlementService.js'; import { IChatRequestVariableEntry, isImageVariableEntry, isLinkVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; -import { IChatFollowup } from '../common/chatService.js'; +import { IChatFollowup, IChatService } from '../common/chatService.js'; import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; +import { ChatMode } from '../common/constants.js'; import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; -import { CancelAction, ChatModelPickerActionId, ChatSubmitAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, IToggleAgentModeArgs, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; +import { CancelAction, ChatSubmitAction, ChatSubmitSecondaryAgentAction, ChatSwitchToNextModelActionId, IChatExecuteActionContext, IToggleChatModeArgs, ToggleAgentModeActionId } from './actions/chatExecuteActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; import { PromptAttachmentsCollectionWidget } from './attachments/promptAttachments/promptAttachmentsCollectionWidget.js'; import { IChatWidget } from './chat.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { toChatVariable } from './chatAttachmentModel/chatPromptAttachmentsCollection.js'; -import { hookUpResourceAttachmentDragAndContextMenu, hookUpSymbolAttachmentDragAndContextMenu } from './chatContentParts/chatAttachmentsContentPart.js'; +import { DefaultChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, LinkAttachmentWidget, PasteAttachmentWidget } from './chatAttachmentWidgets.js'; import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; -import { ChatDragAndDrop, EditsDragAndDrop } from './chatDragAndDrop.js'; +import { ChatDragAndDrop } from './chatDragAndDrop.js'; import { ChatEditingRemoveAllFilesAction, ChatEditingShowChangesAction } from './chatEditing/chatEditingActions.js'; import { ChatFollowups } from './chatFollowups.js'; +import { ChatSelectedTools } from './chatSelectedTools.js'; import { IChatViewState } from './chatWidget.js'; import { ChatFileReference } from './contrib/chatDynamicVariables/chatFileReference.js'; import { ChatImplicitContext } from './contrib/chatImplicitContext.js'; +import { ChatRelatedFiles } from './contrib/chatInputRelatedFilesContrib.js'; import { resizeImage } from './imageUtils.js'; const $ = dom.$; @@ -124,11 +118,11 @@ interface IChatInputPartOptions { editorOverflowWidgetsDomNode?: HTMLElement; renderWorkingSet?: boolean; enableImplicitContext?: boolean; + supportsChangingModes?: boolean; } export interface IWorkingSetEntry { uri: URI; - isMarkedReadonly?: boolean; } export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { @@ -158,6 +152,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._attachmentModel; } + static selectedToolsModel: ChatSelectedTools; + public getAttachedAndImplicitContext(sessionId: string): IChatRequestVariableEntry[] { const contextArr = [...this.attachmentModel.attachments]; if (this.implicitContext?.enabled && this.implicitContext.value) { @@ -184,7 +180,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }); } - // factor in nested file references into the implicit context + // factor in nested file links of a prompt into the implicit context const variables = this.variableService.getDynamicVariables(sessionId); for (const variable of variables) { if (!(variable instanceof ChatFileReference)) { @@ -198,10 +194,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return toChatVariable(link, false); }), ); - // then add the root reference itself - contextArr.push( - toChatVariable(variable, true), - ); } contextArr @@ -224,6 +216,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._implicitContext; } + private _relatedFiles: ChatRelatedFiles | undefined; + public get relatedFiles(): ChatRelatedFiles | undefined { + return this._relatedFiles; + } + private _hasFileAttachmentContextKey: IContextKey; private readonly _onDidChangeVisibility = this._register(new Emitter()); @@ -268,6 +265,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private executeToolbar!: MenuWorkbenchToolBar; private inputActionsToolbar!: MenuWorkbenchToolBar; + private addFilesToolbar: MenuWorkbenchToolBar | undefined; + get inputEditor() { return this._inputEditor; } @@ -285,6 +284,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge * Context key is set when prompt instructions are attached. */ private promptInstructionsAttached: IContextKey; + private chatMode: IContextKey; private readonly _waitForPersistedLanguageModel = this._register(new MutableDisposable()); private _onDidChangeCurrentLanguageModel = this._register(new Emitter()); @@ -293,6 +293,20 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._currentLanguageModel?.identifier; } + private _onDidChangeCurrentChatMode = this._register(new Emitter()); + readonly onDidChangeCurrentChatMode = this._onDidChangeCurrentChatMode.event; + + private _currentMode: ChatMode = ChatMode.Chat; + public get currentMode(): ChatMode { + if (this.location === ChatAgentLocation.Panel && !this.chatService.unifiedViewEnabled) { + return ChatMode.Chat; + } + + return this._currentMode === ChatMode.Agent && !this.agentService.hasToolsAgent ? + ChatMode.Edit : + this._currentMode; + } + private cachedDimensions: dom.Dimension | undefined; private cachedExecuteToolbarWidth: number | undefined; private cachedInputToolbarWidth: number | undefined; @@ -340,7 +354,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge getContribsInputState: () => any, @IChatWidgetHistoryService private readonly historyService: IChatWidgetHistoryService, @IModelService private readonly modelService: IModelService, - @ILanguageService private readonly languageService: ILanguageService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -348,31 +361,30 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @ILogService private readonly logService: ILogService, - @IHoverService private readonly hoverService: IHoverService, @IFileService private readonly fileService: IFileService, - @ICommandService private readonly commandService: ICommandService, @IEditorService private readonly editorService: IEditorService, - @IOpenerService private readonly openerService: IOpenerService, @IThemeService private readonly themeService: IThemeService, @ITextModelService private readonly textModelResolverService: ITextModelService, @IStorageService private readonly storageService: IStorageService, @ILabelService private readonly labelService: ILabelService, @IChatVariablesService private readonly variableService: IChatVariablesService, - @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IChatAgentService private readonly agentService: IChatAgentService, + @IChatService private readonly chatService: IChatService, ) { super(); this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel)); - if (this.location === ChatAgentLocation.EditingSession) { - this.dnd = this._register(this.instantiationService.createInstance(EditsDragAndDrop, this.attachmentModel, styles)); - } else { - this.dnd = this._register(this.instantiationService.createInstance(ChatDragAndDrop, this.attachmentModel, styles)); + if (!ChatInputPart.selectedToolsModel) { + ChatInputPart.selectedToolsModel = this._register(this.instantiationService.createInstance(ChatSelectedTools)); } + this.dnd = this._register(this.instantiationService.createInstance(ChatDragAndDrop, this._attachmentModel, styles)); + this.getInputState = (): IChatInputState => { return { ...getContribsInputState(), chatContextAttachments: this._attachmentModel.attachments, + chatMode: this._currentMode, }; }; this.inputEditorMaxHeight = this.options.renderStyle === 'compact' ? INPUT_EDITOR_MAX_HEIGHT / 3 : INPUT_EDITOR_MAX_HEIGHT; @@ -381,6 +393,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatCursorAtTop = ChatContextKeys.inputCursorAtTop.bindTo(contextKeyService); this.inputEditorHasFocus = ChatContextKeys.inputHasFocus.bindTo(contextKeyService); this.promptInstructionsAttached = ChatContextKeys.instructionsAttached.bindTo(contextKeyService); + this.chatMode = ChatContextKeys.chatMode.bindTo(contextKeyService); this.history = this.loadHistory(); this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], ChatInputHistoryMaxEntries, historyKeyFn))); @@ -420,8 +433,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (persistedSelection) { const model = this.languageModelsService.lookupLanguageModel(persistedSelection); if (model) { - this._currentLanguageModel = { metadata: model, identifier: persistedSelection }; - this._onDidChangeCurrentLanguageModel.fire(this._currentLanguageModel); + this.setCurrentLanguageModel({ metadata: model, identifier: persistedSelection }); + this.checkModelSupported(); } else { this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => { const persistedModel = e.added?.find(m => m.identifier === persistedSelection); @@ -429,34 +442,55 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._waitForPersistedLanguageModel.clear(); if (persistedModel.metadata.isUserSelectable) { - this._currentLanguageModel = { metadata: persistedModel.metadata, identifier: persistedSelection }; - this._onDidChangeCurrentLanguageModel.fire(this._currentLanguageModel!); + this.setCurrentLanguageModel({ metadata: persistedModel.metadata, identifier: persistedSelection }); + this.checkModelSupported(); } } }); } } - this._register(this.chatAgentService.onDidChangeToolsAgentModeEnabled(() => { - if (this._currentLanguageModel && !this.modelSupportedForDefaultAgent(this._currentLanguageModel)) { - this.setCurrentLanguageModelToDefault(); - } + this._register(this._onDidChangeCurrentChatMode.event(() => { + this.checkModelSupported(); })); } - private supportsVision(): boolean { - return this._currentLanguageModel?.metadata.capabilities?.vision ?? false; + public switchToNextModel(): void { + const models = this.getModels(); + if (models.length > 0) { + const currentIndex = models.findIndex(model => model.identifier === this._currentLanguageModel?.identifier); + const nextIndex = (currentIndex + 1) % models.length; + this.setCurrentLanguageModel(models[nextIndex]); + } + } + + private checkModelSupported(): void { + if (this._currentLanguageModel && !this.modelSupportedForDefaultAgent(this._currentLanguageModel)) { + this.setCurrentLanguageModelToDefault(); + } + } + + setChatMode(mode: ChatMode): void { + if (!this.options.supportsChangingModes) { + return; + } + + this._currentMode = mode; + this.chatMode.set(mode); + this._onDidChangeCurrentChatMode.fire(); } private modelSupportedForDefaultAgent(model: ILanguageModelChatMetadataAndIdentifier): boolean { // Probably this logic could live in configuration on the agent, or somewhere else, if it gets more complex - if (this.chatAgentService.getDefaultAgent(this.location)?.isToolsAgent) { + if (this.currentMode === ChatMode.Agent || (this.currentMode === ChatMode.Edit && this.chatService.unifiedViewEnabled)) { if (this.configurationService.getValue('chat.agent.allModels')) { return true; } + const supportsToolsAgent = typeof model.metadata.capabilities?.agentMode === 'undefined' || model.metadata.capabilities.agentMode; + // Filter out models that don't support tool calling, and models that don't support enough context to have a good experience with the tools agent - return !!model.metadata.capabilities?.toolCalling && model.metadata.maxInputTokens > 40000; + return supportsToolsAgent && !!model.metadata.capabilities?.toolCalling; } return true; @@ -477,21 +511,25 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const model = this.languageModelsService.lookupLanguageModel(id); return model?.isUserSelectable && !model.isDefault; }); - this._currentLanguageModel = hasUserSelectableLanguageModels && defaultLanguageModelId ? + const defaultModel = hasUserSelectableLanguageModels && defaultLanguageModelId ? { metadata: this.languageModelsService.lookupLanguageModel(defaultLanguageModelId)!, identifier: defaultLanguageModelId } : undefined; + if (defaultModel) { + this.setCurrentLanguageModel(defaultModel); + } } - private setCurrentLanguageModelByUser(model: ILanguageModelChatMetadataAndIdentifier) { + private setCurrentLanguageModel(model: ILanguageModelChatMetadataAndIdentifier) { this._currentLanguageModel = model; - // The user changed the language model, so we don't wait for the persisted option to be registered - this._waitForPersistedLanguageModel.clear(); if (this.cachedDimensions) { + // For quick chat and editor chat, relayout because the input may need to shrink to accomodate the model name this.layout(this.cachedDimensions.height, this.cachedDimensions.width); } this.storageService.store(this.getSelectedModelStorageKey(), model.identifier, StorageScope.APPLICATION, StorageTarget.USER); + + this._onDidChangeCurrentLanguageModel.fire(model); } private loadHistory(): HistoryNavigator2 { @@ -524,6 +562,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (state.inputValue) { this.setValue(state.inputValue, false); } + + if (state.inputState?.chatMode) { + this.setChatMode(state.inputState.chatMode); + } else if (this.location === ChatAgentLocation.EditingSession) { + this.setChatMode(ChatMode.Edit); + } + + ChatInputPart.selectedToolsModel.reset(); } logInputHistory(): void { @@ -734,8 +780,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.chat-input-container@inputContainer', [ dom.h('.chat-attachments-container@attachmentsContainer', [ dom.h('.chat-attachment-toolbar@attachmentToolbar'), - dom.h('.chat-attached-context@attachedContextContainer'), dom.h('.chat-related-files@relatedFilesContainer'), + dom.h('.chat-attached-context@attachedContextContainer'), ]), dom.h('.chat-editor-container@editorContainer'), dom.h('.chat-input-toolbars@inputToolbars'), @@ -756,14 +802,20 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const toolbarsContainer = elements.inputToolbars; const attachmentToolbarContainer = elements.attachmentToolbar; this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; - this.renderAttachedContext(); if (this.options.enableImplicitContext) { this._implicitContext = this._register(new ChatImplicitContext()); this._register(this._implicitContext.onDidChangeValue(() => this._handleAttachedContextChange())); } + this.renderAttachedContext(); this._register(this._attachmentModel.onDidChangeContext(() => this._handleAttachedContextChange())); - this.renderChatEditingSessionState(null, widget); + this.renderChatEditingSessionState(null); + + if (this.options.renderWorkingSet) { + this._relatedFiles = this._register(new ChatRelatedFiles()); + this._register(this._relatedFiles.onDidChange(() => this.renderChatRelatedFiles())); + } + this.renderChatRelatedFiles(); this.dnd.addOverlay(container, container); @@ -788,7 +840,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge options.wrappingStrategy = 'advanced'; options.bracketPairColorization = { enabled: false }; options.suggest = { - showIcons: false, + showIcons: true, showSnippets: false, showWords: true, showStatusBar: false, @@ -801,7 +853,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const editorOptions = getSimpleCodeEditorWidgetOptions(); editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, GlyphHoverController.ID, CopyPasteController.ID, LinkDetector.ID])); this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions)); + SuggestController.get(this._inputEditor)?.forceRenderingAbove(); + options.overflowWidgetsDomNode?.classList.add('hideSuggestTextIcons'); + this._inputEditorElement.classList.add('hideSuggestTextIcons'); this._register(this._inputEditor.onDidChangeModelContent(() => { const currentHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight); @@ -839,6 +894,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const hoverDelegate = this._register(createInstantHoverDelegate()); this._register(dom.addStandardDisposableListener(toolbarsContainer, dom.EventType.CLICK, e => this.inputEditor.focus())); + this._register(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.CLICK, e => this.inputEditor.focus())); this.inputActionsToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, MenuId.ChatInput, { telemetrySource: this.options.menus.telemetrySource, menuOptions: { shouldForwardArgs: true }, @@ -866,7 +922,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - if (action.id === ChatModelPickerActionId && action instanceof MenuItemAction) { + if (action.id === ChatSwitchToNextModelActionId && action instanceof MenuItemAction) { if (!this._currentLanguageModel) { this.setCurrentLanguageModelToDefault(); } @@ -875,7 +931,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const itemDelegate: ModelPickerDelegate = { onDidChangeModel: this._onDidChangeCurrentLanguageModel.event, setModel: (model: ILanguageModelChatMetadataAndIdentifier) => { - this.setCurrentLanguageModelByUser(model); + // The user changed the language model, so we don't wait for the persisted option to be registered + this._waitForPersistedLanguageModel.clear(); + this.setCurrentLanguageModel(model); this.renderAttachedContext(); }, getModels: () => this.getModels() @@ -883,7 +941,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this.instantiationService.createInstance(ModelPickerActionViewItem, action, this._currentLanguageModel, itemDelegate); } } else if (action.id === ToggleAgentModeActionId && action instanceof MenuItemAction) { - return this.instantiationService.createInstance(ToggleAgentActionViewItem, action); + const delegate: IModePickerDelegate = { + getMode: () => this.currentMode, + onDidChangeMode: this._onDidChangeCurrentChatMode.event + }; + return this.instantiationService.createInstance(ToggleChatModeActionViewItem, action, delegate); } return undefined; @@ -956,34 +1018,29 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.renderAttachedContext(); })); - if (this.options.renderWorkingSet) { - const attachmentToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, attachmentToolbarContainer, MenuId.ChatInputAttachmentToolbar, { - telemetrySource: this.options.menus.telemetrySource, - label: true, - menuOptions: { shouldForwardArgs: true, renderShortTitle: true }, - hiddenItemStrategy: HiddenItemStrategy.Ignore, - hoverDelegate, - getKeyBinding: () => undefined, - actionViewItemProvider: (action, options) => { - if (action.id === 'workbench.action.chat.editing.attachFiles') { - const viewItem = this.instantiationService.createInstance(AddFilesButton, undefined, action, options); - return viewItem; - } - return undefined; + this.addFilesToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, attachmentToolbarContainer, MenuId.ChatInputAttachmentToolbar, { + telemetrySource: this.options.menus.telemetrySource, + label: true, + menuOptions: { shouldForwardArgs: true, renderShortTitle: true }, + hiddenItemStrategy: HiddenItemStrategy.Ignore, + hoverDelegate, + actionViewItemProvider: (action, options) => { + if (action.id === 'workbench.action.chat.editing.attachContext' || action.id === 'workbench.action.chat.attachContext') { + const viewItem = this.instantiationService.createInstance(AddFilesButton, undefined, action, options); + return viewItem; } - })); - attachmentToolbar.context = { widget, placeholder: localize('chatAttachFiles', 'Search for files and context to add to your request') }; - this._register(attachmentToolbar.onDidChangeMenuItems(() => { - if (this.cachedDimensions) { - this.layout(this.cachedDimensions.height, this.cachedDimensions.width); - } - })); - } else { - dom.hide(attachmentToolbarContainer); - } + return undefined; + } + })); + this.addFilesToolbar.context = { widget, placeholder: localize('chatAttachFiles', 'Search for files and context to add to your request') }; + this._register(this.addFilesToolbar.onDidChangeMenuItems(() => { + if (this.cachedDimensions) { + this.layout(this.cachedDimensions.height, this.cachedDimensions.width); + } + })); } - private async renderAttachedContext() { + private renderAttachedContext() { const container = this.attachedContextContainer; const oldHeight = container.offsetHeight; const store = new DisposableStore(); @@ -993,7 +1050,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const hoverDelegate = store.add(createInstantHoverDelegate()); const attachments = [...this.attachmentModel.attachments.entries()]; - dom.setVisibility(Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.instructionAttachmentsPart.empty, this.attachedContextContainer); + const hasAttachments = Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.instructionAttachmentsPart.empty; + dom.setVisibility(Boolean(hasAttachments || (this.addFilesToolbar && !this.addFilesToolbar.isEmpty())), this.attachmentsContainer); + dom.setVisibility(hasAttachments, this.attachedContextContainer); if (!attachments.length) { this._indexOfLastAttachedContextDeletedWithKeyboard = -1; } @@ -1006,160 +1065,27 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.promptInstructionsAttached.set(!this.instructionAttachmentsPart.empty); this.instructionAttachmentsPart.render(container); - const attachmentInitPromises: Promise[] = []; for (const [index, attachment] of attachments) { - const widget = dom.append(container, $('.chat-attached-context-attachment.show-file-icons')); - const label = this._contextResourceLabels.create(widget, { supportIcons: true, hoverDelegate, hoverTargetOverride: widget }); + const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; + const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; + const shouldFocusClearButton = index === Math.min(this._indexOfLastAttachedContextDeletedWithKeyboard, this.attachmentModel.size - 1); - let ariaLabel: string | undefined; - - let resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; - let range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; + let attachmentWidget; if (resource && (attachment.isFile || attachment.isDirectory)) { - const fileBasename = basename(resource.path); - const fileDirname = dirname(resource.path); - const friendlyName = `${fileBasename} ${fileDirname}`; - ariaLabel = range ? localize('chat.fileAttachmentWithRange', "Attached file, {0}, line {1} to line {2}", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment', "Attached file, {0}", friendlyName); - - if (attachment.isOmitted) { - this.customAttachment(widget, friendlyName, hoverDelegate, ariaLabel, store); - } else { - const fileOptions: IFileLabelOptions = { hidePath: true }; - label.setFile(resource, attachment.isFile ? { - ...fileOptions, - fileKind: FileKind.FILE, - range, - } : { - ...fileOptions, - fileKind: FileKind.FOLDER, - icon: !this.themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined - }); - } - - this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); - this.instantiationService.invokeFunction(accessor => { - if (resource) { - store.add(hookUpResourceAttachmentDragAndContextMenu(accessor, widget, resource)); - } - }); + attachmentWidget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); } else if (attachment.isImage) { - ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name); - const supportsVision = this.supportsVision(); - const hoverElement = this.customAttachment(widget, attachment.name, hoverDelegate, ariaLabel, store, attachment.isImage, supportsVision); - - if (attachment.references) { - widget.style.cursor = 'pointer'; - const clickHandler = () => { - if (attachment.references && URI.isUri(attachment.references[0].reference)) { - this.openResource(attachment.references[0].reference, false, undefined); - } - }; - store.add(addDisposableListener(widget, 'click', clickHandler)); - } - - if (supportsVision) { - attachmentInitPromises.push(Promises.withAsyncBody(async (resolve) => { - let buffer: Uint8Array; - try { - if (attachment.value instanceof URI) { - const readFile = await this.fileService.readFile(attachment.value); - if (store.isDisposed) { - return; - } - buffer = readFile.value.buffer; - } else { - buffer = attachment.value as Uint8Array; - } - this.createImageElements(buffer, widget, hoverElement); - } catch (error) { - console.error('Error processing attachment:', error); - } - store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: false })); - resolve(); - })); - } - - this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); - widget.style.position = 'relative'; + attachmentWidget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); } else if (isPasteVariableEntry(attachment)) { - ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); - - const classNames = ['file-icon', `${attachment.language}-lang-file-icon`]; - if (attachment.copiedFrom) { - resource = attachment.copiedFrom.uri; - range = attachment.copiedFrom.range; - const filename = basename(resource.path); - label.setLabel(filename, undefined, { extraClasses: classNames }); - } else { - label.setLabel(attachment.fileName, undefined, { extraClasses: classNames }); - } - widget.appendChild(dom.$('span.attachment-additional-info', {}, `Pasted ${attachment.pastedLines}`)); - - widget.style.position = 'relative'; - - const hoverContent: IManagedHoverTooltipMarkdownString = { - markdown: { - value: `${attachment.copiedFrom ? this.labelService.getUriLabel(attachment.copiedFrom.uri, { relative: true }) : attachment.fileName}\n\n---\n\n\`\`\`${attachment.language}\n\n${attachment.code}\n\`\`\``, - }, - markdownNotSupportedFallback: attachment.code, - }; - store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverContent, { trapFocus: true })); - this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); - - const copiedFromResource = attachment.copiedFrom?.uri; - if (copiedFromResource) { - store.add(this.instantiationService.invokeFunction(accessor => hookUpResourceAttachmentDragAndContextMenu(accessor, widget, copiedFromResource))); - } + attachmentWidget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); } else if (isLinkVariableEntry(attachment)) { - ariaLabel = localize('chat.attachment.link', "Attached link, {0}", attachment.name); - label.setResource({ resource: attachment.value, name: attachment.name }, { icon: Codicon.link, title: attachment.value.toString() }); - this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); + attachmentWidget = this.instantiationService.createInstance(LinkAttachmentWidget, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); } else { - const attachmentLabel = attachment.fullName ?? attachment.name; - const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel; - label.setLabel(withIcon, undefined); - - ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); - - this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); + attachmentWidget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); } - - if (attachment.kind === 'symbol') { - const scopedContextKeyService = store.add(this.contextKeyService.createScoped(widget)); - store.add(this.instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, widget, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext))); - } - - await Promise.all(attachmentInitPromises); - if (store.isDisposed) { - return; - } - - if (resource) { - widget.style.cursor = 'pointer'; - store.add(dom.addDisposableListener(widget, dom.EventType.CLICK, (e: MouseEvent) => { - dom.EventHelper.stop(e, true); - if (attachment.isDirectory) { - this.openResource(resource, true); - } else { - this.openResource(resource, false, range); - } - })); - - store.add(dom.addDisposableListener(widget, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - dom.EventHelper.stop(e, true); - if (attachment.isDirectory) { - this.openResource(resource, true); - } else { - this.openResource(resource, false, range); - } - } - })); - } - - widget.tabIndex = 0; - widget.ariaLabel = ariaLabel; + store.add(attachmentWidget); + store.add(attachmentWidget.onDidDelete((e) => { + this.handleAttachmentDeletion(e, index, attachment); + })); } if (oldHeight !== container.offsetHeight) { @@ -1167,119 +1093,27 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - private customAttachment(widget: HTMLElement, friendlyName: string, hoverDelegate: IHoverDelegate, ariaLabel: string, store: DisposableStore, isImage?: boolean, supportsVision?: boolean): HTMLElement { - const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$(supportsVision ? 'span.codicon.codicon-file-media' : 'span.codicon.codicon-warning')); - const textLabel = dom.$('span.chat-attached-context-custom-text', {}, friendlyName); - widget.appendChild(pillIcon); - widget.appendChild(textLabel); + private handleAttachmentDeletion(e: globalThis.Event, index: number, attachment: IChatRequestVariableEntry) { + this._attachmentModel.delete(attachment.id); - const hoverElement = dom.$('div.chat-attached-context-hover'); - hoverElement.setAttribute('aria-label', ariaLabel); - - if (!supportsVision) { - widget.classList.add('warning'); - hoverElement.textContent = localize('chat.fileAttachmentHover', "{0} does not support this {1} type.", this.currentLanguageModel ? this.languageModelsService.lookupLanguageModel(this.currentLanguageModel)?.name : this.currentLanguageModel, isImage ? 'image' : 'file'); - store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: true })); - } - - return hoverElement; - } - - private openResource(resource: URI, isDirectory: true): void; - private openResource(resource: URI, isDirectory: false, range: IRange | undefined): void; - private openResource(resource: URI, isDirectory?: boolean, range?: IRange): void { - if (isDirectory) { - // Reveal Directory in explorer - this.commandService.executeCommand(revealInSideBarCommand.id, resource); - return; - } - - // Open file in editor - const openTextEditorOptions: ITextEditorOptions | undefined = range ? { selection: range } : undefined; - const options: OpenInternalOptions = { - fromUserGesture: true, - editorOptions: openTextEditorOptions, - }; - this.openerService.open(resource, options); - } - - private attachButtonAndDisposables(widget: HTMLElement, index: number, attachment: IChatRequestVariableEntry, hoverDelegate: IHoverDelegate) { - const store = this.attachedContextDisposables.value; - if (!store) { - return; - } - - const clearButton = new Button(widget, { - supportIcons: true, - hoverDelegate, - title: localize('chat.attachment.clearButton', "Remove from context") - }); - - // If this item is rendering in place of the last attached context item, focus the clear button so the user can continue deleting attached context items with the keyboard - if (index === Math.min(this._indexOfLastAttachedContextDeletedWithKeyboard, this.attachmentModel.size - 1)) { - clearButton.focus(); - } - - store.add(clearButton); - clearButton.icon = Codicon.close; - store.add(Event.once(clearButton.onDidClick)((e) => { - this._attachmentModel.delete(attachment.id); - - // Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click) - if (dom.isKeyboardEvent(e)) { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - this._indexOfLastAttachedContextDeletedWithKeyboard = index; - } + // Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click) + if (dom.isKeyboardEvent(e)) { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + this._indexOfLastAttachedContextDeletedWithKeyboard = index; } - - if (this._attachmentModel.size === 0) { - this.focus(); - } - - this._onDidChangeContext.fire({ removed: [attachment] }); - })); - } - - // Helper function to create and replace image - private createImageElements(buffer: ArrayBuffer | Uint8Array, widget: HTMLElement, hoverElement: HTMLElement) { - const blob = new Blob([buffer], { type: 'image/png' }); - const url = URL.createObjectURL(blob); - const pillImg = dom.$('img.chat-attached-context-pill-image', { src: url, alt: '' }); - const pill = dom.$('div.chat-attached-context-pill', {}, pillImg); - - const existingPill = widget.querySelector('.chat-attached-context-pill'); - if (existingPill) { - existingPill.replaceWith(pill); } - const hoverImage = dom.$('img.chat-attached-context-image', { src: url, alt: '' }); + if (this._attachmentModel.size === 0) { + this.focus(); + } - // Update hover image - hoverElement.appendChild(hoverImage); - - hoverImage.onload = () => { - URL.revokeObjectURL(url); - }; - - hoverImage.onerror = () => { - // reset to original icon on error or invalid image - const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-file-media')); - const pill = dom.$('div.chat-attached-context-pill', {}, pillIcon); - const existingPill = widget.querySelector('.chat-attached-context-pill'); - if (existingPill) { - existingPill.replaceWith(pill); - } - }; + this._onDidChangeContext.fire({ removed: [attachment] }); } - async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null, chatWidget?: IChatWidget) { + async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null) { dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer); - if (chatEditingSession && this.configurationService.getValue('chat.renderRelatedFiles')) { - this.renderChatRelatedFiles(chatEditingSession, this.relatedFilesContainer); - } - const seenEntries = new ResourceSet(); const entries: IChatCollapsibleListItem[] = chatEditingSession?.entries.get().map((entry) => { seenEntries.add(entry.modifiedURI); @@ -1299,16 +1133,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Summary of number of files changed const innerContainer = this.chatEditingSessionWidgetContainer.querySelector('.chat-editing-session-container.show-file-icons') as HTMLElement ?? dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons')); - for (const [file, metadata] of chatEditingSession.workingSet.entries()) { - if (!seenEntries.has(file) && metadata.state !== WorkingSetEntryState.Suggested) { + for (const entry of chatEditingSession.entries.get()) { + if (!seenEntries.has(entry.modifiedURI)) { entries.unshift({ - reference: file, - state: metadata.state, - description: metadata.description, + reference: entry.modifiedURI, + state: entry.state.get(), kind: 'reference', - isMarkedReadonly: metadata.isMarkedReadonly, }); - seenEntries.add(file); + seenEntries.add(entry.modifiedURI); } } @@ -1326,19 +1158,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const overviewTitle = overviewRegion.querySelector('.working-set-title') as HTMLElement ?? dom.append(overviewRegion, $('.working-set-title')); const overviewFileCount = overviewTitle.querySelector('span.working-set-count') ?? dom.append(overviewTitle, $('span.working-set-count')); - let suggestedFilesInWorkingSetCount = 0; - overviewFileCount.textContent = ''; - if (entries.length === 1) { - overviewFileCount.textContent = localize('chatEditingSession.oneFile.1', '1 file changed'); - suggestedFilesInWorkingSetCount = entries[0].kind === 'reference' && entries[0].state === WorkingSetEntryState.Suggested ? 1 : 0; - } else { - suggestedFilesInWorkingSetCount = entries.filter(e => e.kind === 'reference' && e.state === WorkingSetEntryState.Suggested).length; - } - - if (entries.length > 1) { - const fileCount = entries.length - suggestedFilesInWorkingSetCount; - overviewFileCount.textContent = (fileCount === 1 ? localize('chatEditingSession.oneFile.1', '1 file changed') : localize('chatEditingSession.manyFiles.1', '{0} files changed', fileCount)); - } + overviewFileCount.textContent = entries.length === 1 ? localize('chatEditingSession.oneFile.1', '1 file changed') : localize('chatEditingSession.manyFiles.1', '{0} files changed', entries.length); overviewTitle.ariaLabel = overviewFileCount.textContent; overviewTitle.tabIndex = 0; @@ -1410,26 +1230,30 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._onDidChangeHeight.fire(); } - async renderChatRelatedFiles(chatEditingSession: IChatEditingSession, anchor: HTMLElement) { + async renderChatRelatedFiles() { + const anchor = this.relatedFilesContainer; dom.clearNode(anchor); + const shouldRender = this.configurationService.getValue('chat.renderRelatedFiles'); + dom.setVisibility(Boolean(this.relatedFiles?.value.length && shouldRender), anchor); + if (!shouldRender || !this.relatedFiles?.value.length) { + return; + } + const hoverDelegate = getDefaultHoverDelegate('element'); - for (const [uri, metadata] of chatEditingSession.workingSet) { - if (metadata.state !== WorkingSetEntryState.Suggested) { - continue; - } + for (const { uri, description } of this.relatedFiles.value) { const uriLabel = this._chatEditsActionsDisposables.add(new Button(anchor, { supportIcons: true, secondary: true, hoverDelegate })); uriLabel.label = this.labelService.getUriBasenameLabel(uri); - uriLabel.element.classList.add('monaco-icon-label', ...getIconClasses(this.modelService, this.languageService, uri, FileKind.FILE)); - uriLabel.element.title = localize('suggeste.title', "{0} - {1}", this.labelService.getUriLabel(uri, { relative: true }), metadata.description ?? ''); + uriLabel.element.classList.add('monaco-icon-label'); + uriLabel.element.title = localize('suggeste.title', "{0} - {1}", this.labelService.getUriLabel(uri, { relative: true }), description ?? ''); this._chatEditsActionsDisposables.add(uriLabel.onDidClick(() => { group.remove(); // REMOVE asap this._attachmentModel.addFile(uri); - chatEditingSession.remove(WorkingSetEntryRemovalReason.User, uri); + this.relatedFiles?.remove(uri); })); const addButton = this._chatEditsActionsDisposables.add(new Button(anchor, { @@ -1443,17 +1267,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._chatEditsActionsDisposables.add(addButton.onDidClick(() => { group.remove(); // REMOVE asap this._attachmentModel.addFile(uri); - chatEditingSession.remove(WorkingSetEntryRemovalReason.User, uri); + this.relatedFiles?.remove(uri); })); const sep = document.createElement('div'); sep.classList.add('separator'); const group = document.createElement('span'); - group.classList.add('monaco-button-dropdown', 'sidebyside-button', 'show-file-icons'); - group.appendChild(uriLabel.element); - group.appendChild(sep); + group.classList.add('monaco-button-dropdown', 'sidebyside-button'); group.appendChild(addButton.element); + group.appendChild(sep); + group.appendChild(uriLabel.element); dom.append(anchor, group); this._chatEditsActionsDisposables.add(toDisposable(() => { @@ -1526,7 +1350,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight), inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 32, inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : 28, - attachmentsHeight: this.attachmentsContainer.offsetHeight + 6, + attachmentsHeight: this.attachmentsContainer.offsetHeight + (this.attachmentsContainer.checkVisibility() ? 6 : 0), editorBorder: 2, inputPartHorizontalPaddingInside: 12, toolbarsWidth: this.options.renderStyle === 'compact' ? executeToolbarWidth + executeToolbarPadding + inputToolbarWidth + inputToolbarPadding : 0, @@ -1622,6 +1446,7 @@ class ModelPickerActionViewItem extends DropdownMenuActionViewItemWithKeybinding @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService, @ICommandService commandService: ICommandService, ) { const modelActionsProvider: IActionProvider = { @@ -1644,16 +1469,21 @@ class ModelPickerActionViewItem extends DropdownMenuActionViewItemWithKeybinding const models: ILanguageModelChatMetadataAndIdentifier[] = this.delegate.getModels(); const actions = models.map(entry => setLanguageModelAction(entry)); - if (contextKeyService.getContextKeyValue(ChatContextKeys.Setup.limited.key) === true) { + if (chatEntitlementService.entitlement === ChatEntitlement.Limited) { actions.push(new Separator()); - actions.push(toAction({ id: 'moreModels', label: localize('chat.moreModels', "Add More Models..."), run: () => commandService.executeCommand('workbench.action.chat.upgradePlan') })); + actions.push(toAction({ id: 'moreModels', label: localize('chat.moreModels', "Add More Models..."), run: () => commandService.executeCommand('workbench.action.chat.upgradePlan', 'chat-models') })); } return actions; } }; - super(action, modelActionsProvider, contextMenuService, undefined, keybindingService, contextKeyService); + const actionWithLabel: IAction = { + ...action, + tooltip: localize('chat.modelPicker.label', "Pick Model"), + run: () => { } + }; + super(actionWithLabel, modelActionsProvider, contextMenuService, undefined, keybindingService, contextKeyService); this._register(delegate.onDidChangeModel(modelId => { this.currentLanguageModel = modelId; this.renderLabel(this.element!); @@ -1668,58 +1498,82 @@ class ModelPickerActionViewItem extends DropdownMenuActionViewItemWithKeybinding override render(container: HTMLElement): void { super.render(container); - container.classList.add('chat-modelPicker-item'); + container.classList.add('chat-modelPicker-item', 'chat-dropdown-item'); } } const chatInputEditorContainerSelector = '.interactive-input-editor'; setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector); -class ToggleAgentActionViewItem extends DropdownMenuActionViewItemWithKeybinding { - private readonly agentStateActions: IAction[]; +interface IModePickerDelegate { + onDidChangeMode: Event; + getMode(): ChatMode; +} +class ToggleChatModeActionViewItem extends DropdownMenuActionViewItemWithKeybinding { constructor( action: MenuItemAction, + private readonly delegate: IModePickerDelegate, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, + @IChatService chatService: IChatService, ) { - const agentStateActions = [ - { - ...action, - id: 'agentMode', - label: localize('chat.agentMode', "Agent"), - class: undefined, - enabled: true, - run: () => action.run({ agentMode: true } satisfies IToggleAgentModeArgs) - }, - { - ...action, - id: 'normalMode', - label: localize('chat.normalMode', "Edit"), - class: undefined, - enabled: true, - checked: !action.checked, - run: () => action.run({ agentMode: false } satisfies IToggleAgentModeArgs) - }, - ]; + const makeAction = (mode: ChatMode): IAction => ({ + ...action, + id: mode, + label: this.modeToString(mode), + class: undefined, + enabled: true, + checked: delegate.getMode() === mode, + run: async () => { + const result = await action.run({ mode } satisfies IToggleChatModeArgs); + this.renderLabel(this.element!); + return result; + } + }); - super(action, agentStateActions, contextMenuService, undefined, keybindingService, contextKeyService); - this.agentStateActions = agentStateActions; + const actionProvider: IActionProvider = { + getActions: () => { + const agentStateActions = [ + makeAction(ChatMode.Agent), + makeAction(ChatMode.Edit), + ]; + if (chatService.unifiedViewEnabled) { + agentStateActions.unshift(makeAction(ChatMode.Chat)); + } + + return agentStateActions; + } + }; + + super(action, actionProvider, contextMenuService, undefined, keybindingService, contextKeyService); + this._register(delegate.onDidChangeMode(() => this.renderLabel(this.element!))); + } + + private modeToString(mode: ChatMode) { + switch (mode) { + case ChatMode.Agent: + return localize('chat.agentMode', "Agent"); + case ChatMode.Edit: + return localize('chat.normalMode', "Edit"); + case ChatMode.Chat: + return localize('chat.chatMode', "Chat"); + } } protected override renderLabel(element: HTMLElement): IDisposable | null { // Can't call super.renderLabel because it has a hack of forcing the 'codicon' class this.setAriaLabelAttributes(element); - const state = this.agentStateActions.find(action => action.checked)?.label ?? ''; + const state = this.modeToString(this.delegate.getMode()); dom.reset(element, dom.$('span.chat-model-label', undefined, state), ...renderLabelWithIcons(`$(chevron-down)`)); return null; } override render(container: HTMLElement): void { super.render(container); - container.classList.add('chat-modelPicker-item'); + container.classList.add('chat-dropdown-item'); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 8d1682e18c5..2ecff47e39b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -17,8 +17,9 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Iterable } from '../../../../base/common/iterator.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore, IDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, dispose, thenIfNotDisposed, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { FileAccess } from '../../../../base/common/network.js'; import { clamp } from '../../../../base/common/numbers.js'; @@ -42,14 +43,16 @@ import { ColorScheme } from '../../../../platform/theme/common/theme.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IWorkbenchIssueService } from '../../issue/common/issue.js'; import { annotateSpecialMarkdownContent } from '../common/annotations.js'; -import { ChatAgentLocation, IChatAgentMetadata } from '../common/chatAgents.js'; +import { checkModeOption } from '../common/chat.js'; +import { IChatAgentMetadata } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatRequestVariableEntry, IChatTextEditGroup } from '../common/chatModel.js'; import { chatSubcommandLeader } from '../common/chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatConfirmation, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatTask, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatConfirmation, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatService, IChatTask, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js'; import { IChatCodeCitations, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatWorkingProgress, isRequestVM, isResponseVM } from '../common/chatViewModel.js'; import { getNWords } from '../common/chatWordCounter.js'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js'; +import { ChatMode } from '../common/constants.js'; import { MarkUnhelpfulActionId } from './actions/chatTitleActions.js'; import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidgetService } from './chat.js'; import { ChatAgentHover, getChatAgentHoverOptions } from './chatAgentHover.js'; @@ -104,6 +107,7 @@ const forceVerboseLayoutTracing = false export interface IChatRendererDelegate { container: HTMLElement; getListLength(): number; + currentChatMode(): ChatMode; readonly onDidScroll?: Event; } @@ -132,6 +136,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer = this._onDidChangeItemHeight.event; private readonly _editorPool: EditorPool; + private readonly _toolEditorPool: EditorPool; private readonly _diffEditorPool: DiffEditorPool; private readonly _treePool: TreePool; private readonly _contentReferencesListPool: CollapsibleListPool; @@ -142,7 +147,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - return this._editorPool.inUse(); + return Iterable.concat(this._editorPool.inUse(), this._toolEditorPool.inUse()); } private traceLayout(method: string, message: string) { @@ -240,10 +247,13 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); } else { - const renderedError = this.instantiationService.createInstance(ChatWarningContentPart, element.errorDetails.responseIsFiltered ? 'info' : 'error', new MarkdownString(element.errorDetails.message), this.renderer); + const level = element.errorDetails.level ?? (element.errorDetails.responseIsFiltered ? ChatErrorLevel.Info : ChatErrorLevel.Error); + const renderedError = this.instantiationService.createInstance(ChatWarningContentPart, level, new MarkdownString(element.errorDetails.message), this.renderer); templateData.elementDisposables.add(renderedError); templateData.value.appendChild(renderedError.domNode); } @@ -812,10 +821,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer part.kind === 'toolInvocation' && !part.isComplete))) { @@ -872,7 +885,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer content.kind === other.kind); } private renderUndoStop(content: IChatUndoStop) { - return this.renderNoContent(other => other.kind === 'undoStop' && other.id === content.id); + return this.renderNoContent(other => other.kind === content.kind && other.id === content.id); } private renderNoContent(equals: (otherContent: IChatRendererContent) => boolean): IChatContentPart { @@ -936,7 +948,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { this.updateItemHeight(templateData); })); @@ -975,7 +987,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { codeBlocksByResponseId[codeBlockStartIndex + i] = info; - info.uriPromise.then(uri => { + part.addDisposable!(thenIfNotDisposed(info.uriPromise, uri => { if (!uri) { return; } @@ -987,14 +999,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._currentLayoutWidth, this._toolInvocationCodeBlockCollection, codeBlockStartIndex); + const part = this.instantiationService.createInstance(ChatToolInvocationPart, toolInvocation, context, this.renderer, this._contentReferencesListPool, this._toolEditorPool, () => this._currentLayoutWidth, this._toolInvocationCodeBlockCollection, codeBlockStartIndex); part.addDisposable(part.onDidChangeHeight(() => { this.updateItemHeight(templateData); })); @@ -1043,7 +1055,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 0c3186b3b9f..3b763727a52 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -193,7 +193,8 @@ export class ChatMarkdownDecorationsRenderer { { location: widget.location, agentId: agent.id, - userSelectedModelId: widget.input.currentLanguageModel + userSelectedModelId: widget.input.currentLanguageModel, + mode: widget.input.currentMode }); })); } else { @@ -229,7 +230,8 @@ export class ChatMarkdownDecorationsRenderer { location: widget.location, agentId: agent.id, slashCommand: args.command, - userSelectedModelId: widget.input.currentLanguageModel + userSelectedModelId: widget.input.currentLanguageModel, + mode: widget.input.currentMode }); })); diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index 3256369a754..cc8326cca63 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -21,7 +21,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from '../../../common/views.js'; -import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions } from '../../../services/extensionManagement/common/extensionFeatures.js'; +import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../services/extensionManagement/common/extensionFeatures.js'; import { isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; import * as extensionsRegistry from '../../../services/extensions/common/extensionsRegistry.js'; import { showExtensionsWithIdsCommandId } from '../../extensions/browser/extensionsActions.js'; @@ -29,6 +29,7 @@ import { IExtension, IExtensionsWorkbenchService } from '../../extensions/common import { ChatAgentLocation, IChatAgentData, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IRawChatParticipantContribution } from '../common/chatParticipantContribTypes.js'; +import { ChatConfiguration } from '../common/constants.js'; import { ChatViewId } from './chat.js'; import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from './chatViewPane.js'; @@ -36,7 +37,7 @@ import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID, ChatViewPane } fr const chatViewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: CHAT_SIDEBAR_PANEL_ID, - title: localize2('chat.viewContainer.label', "Chat"), + title: { value: 'Copilot', original: 'Copilot' }, icon: Codicon.commentDiscussion, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]), storageId: CHAT_SIDEBAR_PANEL_ID, @@ -49,7 +50,7 @@ const chatViewDescriptor: IViewDescriptor[] = [{ containerIcon: chatViewContainer.icon, containerTitle: chatViewContainer.title.value, singleViewPaneContainerTitle: chatViewContainer.title.value, - name: localize2('chat.viewContainer.label', "Chat"), + name: { value: 'Copilot', original: 'Copilot' }, canToggleVisibility: false, canMoveView: true, openCommandActionDescriptor: { @@ -107,10 +108,13 @@ const editsViewDescriptor: IViewDescriptor[] = [{ order: 2 }, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]), - when: ContextKeyExpr.or( - ChatContextKeys.Setup.hidden.negate(), - ChatContextKeys.Setup.installed, - ChatContextKeys.editingParticipantRegistered + when: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${ChatConfiguration.UnifiedChatView}`).negate(), + ContextKeyExpr.or( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.installed, + ChatContextKeys.editingParticipantRegistered + ) ) }]; Registry.as(ViewExtensions.ViewsRegistry).registerViews(editsViewDescriptor, editsViewContainer); diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index a4af4355058..137fc14757b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -19,12 +19,12 @@ import { IQuickInputService, IQuickWidget } from '../../../../platform/quickinpu import { editorBackground, inputBackground, quickInputBackground, quickInputForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { IQuickChatOpenOptions, IQuickChatService, showChatView } from './chat.js'; import { ChatWidget } from './chatWidget.js'; -import { ChatAgentLocation } from '../common/chatAgents.js'; import { ChatModel, isCellTextEditOperation } from '../common/chatModel.js'; import { IParsedChatRequest } from '../common/chatParserTypes.js'; import { IChatProgress, IChatService } from '../common/chatService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { EDITOR_DRAG_AND_DROP_BACKGROUND } from '../../../common/theme.js'; +import { ChatAgentLocation } from '../common/constants.js'; export class QuickChatService extends Disposable implements IQuickChatService { readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts b/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts new file mode 100644 index 00000000000..57ae4a9faf2 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { reset } from '../../../../base/browser/dom.js'; +import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { Event } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { assertType } from '../../../../base/common/types.js'; +import { localize } from '../../../../nls.js'; +import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; +import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILanguageModelToolsService, IToolData } from '../common/languageModelToolsService.js'; +import { AttachToolsAction } from './actions/chatToolActions.js'; + +export class ChatSelectedTools extends Disposable { + + private readonly _selectedTools = observableValue(this, undefined); + + readonly tools: IObservable; + + constructor( + @IActionViewItemService actionViewItemService: IActionViewItemService, + @ILanguageModelToolsService toolsService: ILanguageModelToolsService, + @IInstantiationService instaService: IInstantiationService + ) { + super(); + + const allTools = observableFromEvent( + toolsService.onDidChangeTools, + () => Array.from(toolsService.getTools()).filter(t => t.canBeReferencedInPrompt) + ); + + this.tools = derived(r => { + const custom = this._selectedTools.read(r); + return custom ?? allTools.read(r); + }); + + const toolsCount = derived(r => { + const count = allTools.read(r).length; + const enabled = this.tools.read(r).length; + return { count, enabled }; + }); + + this._store.add(actionViewItemService.register(MenuId.ChatInputAttachmentToolbar, AttachToolsAction.id, (action, options) => { + if (!(action instanceof MenuItemAction)) { + return undefined; + } + + return instaService.createInstance(class extends MenuEntryActionViewItem { + + override render(container: HTMLElement): void { + this.options.icon = false; + this.options.label = true; + container.classList.add('chat-mcp'); + super.render(container); + } + + protected override updateLabel(): void { + this._store.add(autorun(r => { + assertType(this.label); + + const { enabled, count } = toolsCount.read(r); + + if (count === 0) { + super.updateLabel(); + return; + } + + const message = enabled !== count + ? localize('tool.1', "{0} {1} of {2}", '$(tools)', enabled, count) + : localize('tool.0', "{0} {1}", '$(tools)', count); + reset(this.label, ...renderLabelWithIcons(message)); + })); + } + + }, action, { ...options, keybindingNotRenderedWithLabel: true }); + + }, Event.fromObservable(toolsCount))); + } + + update(tools: IToolData[]): void { + this._selectedTools.set(tools, undefined); + } + + reset(): void { + this._selectedTools.set(undefined, undefined); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index 0fe3d34257f..887b9879747 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -7,70 +7,62 @@ import './media/chatViewSetup.css'; import { $, getActiveElement, setVisibility } from '../../../../base/browser/dom.js'; import { ButtonWithDropdown } from '../../../../base/browser/ui/button/button.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { mainWindow } from '../../../../base/browser/window.js'; import { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; -import { Barrier, timeout } from '../../../../base/common/async.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { timeout } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; +import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Lazy } from '../../../../base/common/lazy.js'; -import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { IRequestContext } from '../../../../base/parts/request/common/request.js'; +import { combinedDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import Severity from '../../../../base/common/severity.js'; +import { StopWatch } from '../../../../base/common/stopwatch.js'; +import { equalsIgnoreCase } from '../../../../base/common/strings.js'; +import { isObject } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { ICustomDialogHTMLElement, IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import product from '../../../../platform/product/common/product.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { asText, IRequestService } from '../../../../platform/request/common/request.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js'; -import { AuthenticationSession, IAuthenticationExtensionsService, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; -import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; +import { AuthenticationSession, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; +import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extensions/browser/extensionUrlHandler.js'; +import { nullExtensionDescription } from '../../../services/extensions/common/extensions.js'; +import { IHostService } from '../../../services/host/browser/host.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; +import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; +import { IStatusbarService } from '../../../services/statusbar/browser/statusbar.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { IExtension, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; -import { IChatAgentService } from '../common/chatAgents.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; +import { ChatAgentLocation, IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService, IChatWelcomeMessageContent } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { CHAT_CATEGORY, CHAT_SETUP_ACTION_ID, CHAT_SETUP_ACTION_LABEL } from './actions/chatActions.js'; +import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService } from '../common/chatEntitlementService.js'; +import { IChatProgress, IChatProgressMessage, IChatWarningMessage } from '../common/chatService.js'; +import { CHAT_CATEGORY, CHAT_SETUP_ACTION_ID } from './actions/chatActions.js'; import { ChatViewId, EditsViewId, ensureSideBarChatViewSize, preferCopilotEditsView, showCopilotView } from './chat.js'; import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js'; import { ChatViewsWelcomeExtensions, IChatViewsWelcomeContributionRegistry } from './viewsWelcome/chatViewsWelcome.js'; -import { IChatQuotasService } from '../common/chatQuotasService.js'; -import { mainWindow } from '../../../../base/browser/window.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IHostService } from '../../../services/host/browser/host.js'; -import Severity from '../../../../base/common/severity.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; -import { isWeb } from '../../../../base/common/platform.js'; -import { ExtensionUrlHandlerOverrideRegistry } from '../../../services/extensions/browser/extensionUrlHandler.js'; -import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; -import { toErrorMessage } from '../../../../base/common/errorMessage.js'; -import { StopWatch } from '../../../../base/common/stopwatch.js'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationNode } from '../../../../platform/configuration/common/configurationRegistry.js'; -import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; -import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; -import { equalsIgnoreCase } from '../../../../base/common/strings.js'; -import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; -import { ChatEntitlement, IChatEntitlements, IChatEntitlementsService } from '../common/chatEntitlementsService.js'; -import { IStatusbarService } from '../../../services/statusbar/browser/statusbar.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -81,53 +73,232 @@ const defaultChat = { skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '', publicCodeMatchesUrl: product.defaultChatAgent?.publicCodeMatchesUrl ?? '', upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '', - providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', enterpriseProviderName: product.defaultChatAgent?.enterpriseProviderName ?? '', - providerSetting: product.defaultChatAgent?.providerSetting ?? '', providerUriSetting: product.defaultChatAgent?.providerUriSetting ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], - entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', - entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '', manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', + completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '', + walkthroughCommand: product.defaultChatAgent?.walkthroughCommand ?? '', + completionsRefreshTokenCommand: product.defaultChatAgent?.completionsRefreshTokenCommand ?? '', + chatRefreshTokenCommand: product.defaultChatAgent?.chatRefreshTokenCommand ?? '', }; -//#region Service +//#region Contribution -export class ChatEntitlementsService extends Disposable implements IChatEntitlementsService { +class SetupChatAgentImplementation implements IChatAgentImplementation { - declare _serviceBrand: undefined; + static register(instantiationService: IInstantiationService, location: ChatAgentLocation, context: ChatEntitlementContext, controller: Lazy): IDisposable { + return instantiationService.invokeFunction(accessor => { + const chatAgentService = accessor.get(IChatAgentService); - readonly context: ChatSetupContext | undefined; - readonly requests: ChatSetupRequests | undefined; + // TODO@bpasero: expand this to more cases (installed, not signed in / not signed up) + const setupChatAgentContext = ContextKeyExpr.and( + ChatContextKeys.Setup.hidden.negate(), + ChatContextKeys.Setup.installed.negate(), + ChatContextKeys.Setup.fromDialog + ); - constructor( - @IInstantiationService instantiationService: IInstantiationService, - @IProductService productService: IProductService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService - ) { - super(); + const id = location === ChatAgentLocation.Panel ? 'setup.chat' : 'setup.edits'; - if ( - !productService.defaultChatAgent || // needs product config - (isWeb && !environmentService.remoteAuthority) // only enabled locally or a remote backend - ) { - return; - } + const welcomeMessageContent: IChatWelcomeMessageContent = location === ChatAgentLocation.Panel ? + { + title: localize('chatTitle', "Ask Copilot"), + message: new MarkdownString(localize('chatMessage', "Copilot is powered by AI, so mistakes are possible. Review output carefully before use.")), + icon: Codicon.copilotLarge + } : + { + title: localize('editsTitle', "Edit with Copilot"), + message: new MarkdownString(localize('editsMessage', "Start your editing session by defining a set of files that you want to work with. Then ask Copilot for the changes you want to make.")), + icon: Codicon.copilotLarge + }; - this.context = this._register(instantiationService.createInstance(ChatSetupContext)); - this.requests = this._register(instantiationService.createInstance(ChatSetupRequests, this.context)); + return combinedDisposable( + chatAgentService.registerAgent(id, { + id, + name: `${defaultChat.providerName} Copilot`, + isDefault: true, + when: setupChatAgentContext?.serialize(), + slashCommands: [], + disambiguation: [], + locations: [location], + metadata: { + welcomeMessageContent + }, + description: location === ChatAgentLocation.Panel ? localize('chatDescription', "Ask Copilot") : localize('editsDescription', "Edit files in your workspace"), + extensionId: nullExtensionDescription.identifier, + extensionDisplayName: nullExtensionDescription.name, + extensionPublisherId: nullExtensionDescription.publisher + }), + chatAgentService.registerAgentImplementation(id, instantiationService.createInstance(SetupChatAgentImplementation, context, controller)) + ); + }); } - async resolve(token: CancellationToken): Promise { - return this.requests?.forceResolveEntitlement(undefined, token); + constructor( + private readonly context: ChatEntitlementContext, + private readonly controller: Lazy, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILogService private readonly logService: ILogService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + ) { } + + async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void): Promise { + this.telemetryService.publicLog2('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'chat' }); + + const dialog = this.instantiationService.createInstance(ChatSetupDialog, this.context); + const result = await dialog.show(); + + // Proceed with setting up Copilot + if (result) { + const listener = this.controller.value.onDidChange(e => { + switch (this.controller.value.step) { + case ChatSetupStep.SigningIn: + progress({ + kind: 'progressMessage', + content: new MarkdownString(localize('setupChatSignIn2', "Signing in to {0}...", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName)), + } satisfies IChatProgressMessage); + break; + case ChatSetupStep.Installing: + progress({ + kind: 'progressMessage', + content: new MarkdownString(localize('installingCopilot', "Getting Copilot Ready...")), + } satisfies IChatProgressMessage); + break; + } + }); + + let success = false; + try { + success = await this.controller.value.setup(); + } catch (error) { + this.logService.error(localize('setupError', "Error during setup: {0}", toErrorMessage(error))); + } finally { + listener.dispose(); + } + + if (success) { + progress({ + kind: 'progressMessage', + content: new MarkdownString(localize('copilotReady', "Copilot is ready to use.")), + } satisfies IChatProgressMessage); + } + else { + progress({ + kind: 'warning', + content: new MarkdownString(localize('copilotSetupError', "Copilot setup failed. [Try again]({0} \"Retry\").", `command:${CHAT_SETUP_ACTION_ID}`), { isTrusted: true }), + } satisfies IChatWarningMessage); + } + } + + // User has cancelled the setup + else { + progress({ + kind: 'warning', + content: new MarkdownString(localize('settingUpCopilotWarning', "You need to [set up Copilot]({0} \"Set up Copilot\") to use Chat.", `command:${CHAT_SETUP_ACTION_ID}`), { isTrusted: true }), + } satisfies IChatWarningMessage); + } + + return {}; } } -//#endregion +class ChatSetupDialog { -//#region Contribution + constructor( + private readonly context: ChatEntitlementContext, + @IDialogService private readonly dialogService: IDialogService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ITelemetryService private readonly telemetryService: ITelemetryService + ) { } + + async show(): Promise { + const res = await this.dialogService.prompt({ + type: 'none', + message: localize('copilotFree', "Welcome to Copilot"), + cancelButton: { + label: localize('cancel', "Cancel"), + run: () => false + }, + buttons: [ + { + label: this.getPrimaryButton(), + run: () => true + }, + ], + custom: { + icon: Codicon.copilotLarge, + htmlDetails: [this.create()] + } + }); + + return res.result; + } + + private getPrimaryButton(): string { + switch (this.context.state.entitlement) { + case ChatEntitlement.Unknown: + return this.context.state.registered ? localize('signUp', "Sign in to Use Copilot") : localize('signUpFree', "Sign in to Use Copilot for Free"); + case ChatEntitlement.Unresolved: + return this.context.state.registered ? localize('startUp', "Use Copilot") : localize('startUpLimited', "Use Copilot for Free"); + case ChatEntitlement.Available: + case ChatEntitlement.Limited: + return localize('startUpLimited', "Use Copilot for Free"); + case ChatEntitlement.Pro: + case ChatEntitlement.Unavailable: + return localize('startUp', "Use Copilot"); + } + } + + private create(): ICustomDialogHTMLElement { + const disposables = new DisposableStore(); + const element = $('.chat-setup-view'); + + const markdown = this.instantiationService.createInstance(MarkdownRenderer, {}); + + // Header + const header = localize({ key: 'header', comment: ['{Locked="[Copilot]({0})"}'] }, "[Copilot]({0}) is your AI pair programmer.", defaultChat.documentationUrl); + element.appendChild($('p', undefined, disposables.add(markdown.render(new MarkdownString(header, { isTrusted: true }))).element)); + element.appendChild( + $('div.chat-features-container', undefined, + $('div', undefined, + $('div.chat-feature-container', undefined, + renderIcon(Codicon.code), + $('span', undefined, localize('featureChat', "Code faster with Completions")) + ), + $('div.chat-feature-container', undefined, + renderIcon(Codicon.editSession), + $('span', undefined, localize('featureEdits', "Build features with Copilot Edits")) + ), + $('div.chat-feature-container', undefined, + renderIcon(Codicon.commentDiscussion), + $('span', undefined, localize('featureExplore', "Explore your codebase with Chat")) + ) + ) + ) + ); + + // Limited SKU + if (this.context.state.entitlement !== ChatEntitlement.Pro && this.context.state.entitlement !== ChatEntitlement.Unavailable) { + const free = localize({ key: 'free', comment: ['{Locked="[]({0})"}'] }, "$(sparkle-filled) We now offer [Copilot for free]({0}).", defaultChat.skusDocumentationUrl); + element.appendChild($('p', undefined, disposables.add(markdown.render(new MarkdownString(free, { isTrusted: true, supportThemeIcons: true }))).element)); + } + + // Terms + const terms = localize({ key: 'terms', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "By continuing, you agree to the [Terms]({0}) and [Privacy Policy]({1}).", defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl); + element.appendChild($('p.legal', undefined, disposables.add(markdown.render(new MarkdownString(terms, { isTrusted: true }))).element)); + + // SKU Settings + if (this.telemetryService.telemetryLevel !== TelemetryLevel.NONE) { + const settings = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "Copilot Free and Pro may show [public code]({0}) suggestions and we may use your data for product improvement. You can change these [settings]({1}) at any time.", defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl); + element.appendChild($('p.legal', undefined, disposables.add(markdown.render(new MarkdownString(settings, { isTrusted: true }))).element)); + } + + return { element, dispose: () => disposables.dispose() }; + } +} export class ChatSetupContribution extends Disposable implements IWorkbenchContribution { @@ -138,26 +309,47 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr @IInstantiationService private readonly instantiationService: IInstantiationService, @ICommandService private readonly commandService: ICommandService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, - @IChatEntitlementsService chatEntitlementsService: ChatEntitlementsService, + @IChatEntitlementService chatEntitlementService: ChatEntitlementService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); - const context = chatEntitlementsService.context; - const requests = chatEntitlementsService.requests; + const context = chatEntitlementService.context?.value; + const requests = chatEntitlementService.requests?.value; if (!context || !requests) { return; // disabled } const controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, context, requests))); - this.registerChatWelcome(controller, context); - this.registerActions(context, requests); + this.registerSetupAgents(context, controller); + this.registerChatWelcome(context, controller); + this.registerActions(context, requests, controller); this.registerUrlLinkHandler(); - this.registerSetting(context); } - private registerChatWelcome(controller: Lazy, context: ChatSetupContext): void { + private registerSetupAgents(context: ChatEntitlementContext, controller: Lazy): void { + const registration = this._register(new MutableDisposable()); + + const updateRegistration = () => { + const disabled = context.state.installed || context.state.hidden || !this.configurationService.getValue('chat.experimental.setupFromDialog'); + if (!disabled && !registration.value) { + registration.value = combinedDisposable( + SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.Panel, context, controller), + SetupChatAgentImplementation.register(this.instantiationService, ChatAgentLocation.EditingSession, context, controller) + ); + } else if (disabled && registration.value) { + registration.clear(); + } + }; + + this._register(Event.runAndSubscribe(Event.any( + context.onDidChange, + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('chat.experimental.setupFromDialog')) + ), () => updateRegistration())); + } + + private registerChatWelcome(context: ChatEntitlementContext, controller: Lazy): void { Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ title: localize('welcomeChat', "Welcome to Copilot"), when: ChatContextKeys.SetupViewCondition, @@ -166,12 +358,16 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr }); } - private registerActions(context: ChatSetupContext, requests: ChatSetupRequests): void { + private registerActions(context: ChatEntitlementContext, requests: ChatEntitlementRequests, controller: Lazy): void { + const chatSetupTriggerContext = ContextKeyExpr.and( + ChatContextKeys.Setup.fromDialog.negate(), // reduce noise when using the skeleton-view approach + ContextKeyExpr.or( + ChatContextKeys.Setup.installed.negate(), + ChatContextKeys.Entitlement.canSignUp + )); + + const CHAT_SETUP_ACTION_LABEL = localize2('triggerChatSetup', "Use AI Features with Copilot for Free..."); - const chatSetupTriggerContext = ContextKeyExpr.or( - ChatContextKeys.Setup.installed.negate(), - ChatContextKeys.Setup.canSignUp - ); class ChatSetupTriggerAction extends Action2 { constructor() { @@ -196,14 +392,50 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr const configurationService = accessor.get(IConfigurationService); const layoutService = accessor.get(IWorkbenchLayoutService); const statusbarService = accessor.get(IStatusbarService); + const instantiationService = accessor.get(IInstantiationService); + const logService = accessor.get(ILogService); + const dialogService = accessor.get(IDialogService); + const commandService = accessor.get(ICommandService); await context.update({ hidden: false }); - showCopilotView(viewsService, layoutService); - ensureSideBarChatViewSize(viewDescriptorService, layoutService, viewsService); + const setupFromDialog = configurationService.getValue('chat.experimental.setupFromDialog'); + if (!setupFromDialog) { + showCopilotView(viewsService, layoutService); + ensureSideBarChatViewSize(viewDescriptorService, layoutService, viewsService); + } statusbarService.updateEntryVisibility('chat.statusBarEntry', true); configurationService.updateValue('chat.commandCenter.enabled', true); + + if (setupFromDialog) { + const dialog = instantiationService.createInstance(ChatSetupDialog, context); + const result = await dialog.show(); + if (result) { + let success = false; + try { + success = await controller.value.setup({ notificationProgress: true }); + + if (success) { + showCopilotView(viewsService, layoutService); + } + } catch (error) { + logService.error(localize('setupError', "Error during setup: {0}", toErrorMessage(error))); + } + + if (!success) { + const { confirmed } = await dialogService.confirm({ + type: Severity.Error, + message: localize('setupErrorDialog', "Copilot setup failed. Would you like to try again?"), + primaryButton: localize('retry', "Retry"), + }); + + if (confirmed) { + commandService.executeCommand(CHAT_SETUP_ACTION_ID); + } + } + } + } } } @@ -218,7 +450,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr title: ChatSetupHideAction.TITLE, f1: true, category: CHAT_CATEGORY, - precondition: ChatContextKeys.Setup.installed.negate(), + precondition: ContextKeyExpr.and(ChatContextKeys.Setup.installed.negate(), ChatContextKeys.Setup.hidden.negate()), menu: { id: MenuId.ChatTitleBarMenu, group: 'z_hide', @@ -270,8 +502,8 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr category: localize2('chat.category', 'Chat'), f1: true, precondition: ContextKeyExpr.or( - ChatContextKeys.Setup.canSignUp, - ChatContextKeys.Setup.limited, + ChatContextKeys.Entitlement.canSignUp, + ChatContextKeys.Entitlement.limited, ), menu: { id: MenuId.ChatTitleBarMenu, @@ -285,13 +517,13 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr }); } - override async run(accessor: ServicesAccessor): Promise { + override async run(accessor: ServicesAccessor, from?: string): Promise { const openerService = accessor.get(IOpenerService); const telemetryService = accessor.get(ITelemetryService); const hostService = accessor.get(IHostService); const commandService = accessor.get(ICommandService); - telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: 'chat' }); + telemetryService.publicLog2('workbenchActionExecuted', { id: this.desc.id, from: from ?? 'chat' }); openerService.open(URI.parse(defaultChat.upgradePlanUrl)); @@ -335,443 +567,6 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } })); } - - private registerSetting(context: ChatSetupContext): void { - const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); - - let lastNode: IConfigurationNode | undefined; - const registerSetting = () => { - const treatmentId = context.state.entitlement === ChatEntitlement.Limited ? - 'chatAgentMaxRequestsFree' : - 'chatAgentMaxRequestsPro'; - this.experimentService.getTreatment(treatmentId).then(value => { - const defaultValue = value ?? (context.state.entitlement === ChatEntitlement.Limited ? 5 : 15); - const node: IConfigurationNode = { - id: 'chatSidebar', - title: localize('interactiveSessionConfigurationTitle', "Chat"), - type: 'object', - properties: { - 'chat.agent.maxRequests': { - type: 'number', - markdownDescription: localize('chat.agent.maxRequests', "The maximum number of requests to allow Copilot Edits to use per-turn in agent mode. When the limit is reached, Copilot will ask the user to confirm that it should keep working. \n\n> **Note**: For users on the Copilot Free plan, note that each agent mode request currently uses one chat request."), - default: defaultValue, - tags: ['experimental'] - }, - } - }; - configurationRegistry.updateConfigurations({ remove: lastNode ? [lastNode] : [], add: [node] }); - lastNode = node; - }); - }; - this._register(Event.runAndSubscribe(Event.debounce(context.onDidChange, () => { }, 1000), () => registerSetting())); - } -} - -//#endregion - -//#region Chat Setup Request Service - -type EntitlementClassification = { - tid: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; comment: 'The anonymized analytics id returned by the service'; endpoint: 'GoogleAnalyticsId' }; - entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; - quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; - quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; - quotaResetDate: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The date the quota will reset' }; - owner: 'bpasero'; - comment: 'Reporting chat setup entitlements'; -}; - -type EntitlementEvent = { - entitlement: ChatEntitlement; - tid: string; - quotaChat: number | undefined; - quotaCompletions: number | undefined; - quotaResetDate: string | undefined; -}; - -interface IEntitlementsResponse { - readonly access_type_sku: string; - readonly assigned_date: string; - readonly can_signup_for_limited: boolean; - readonly chat_enabled: boolean; - readonly analytics_tracking_id: string; - readonly limited_user_quotas?: { - readonly chat: number; - readonly completions: number; - }; - readonly monthly_quotas?: { - readonly chat: number; - readonly completions: number; - }; - readonly limited_user_reset_date: string; -} - -class ChatSetupRequests extends Disposable { - - static providerId(configurationService: IConfigurationService): string { - if (configurationService.getValue(defaultChat.providerSetting) === defaultChat.enterpriseProviderId) { - return defaultChat.enterpriseProviderId; - } - - return defaultChat.providerId; - } - - private state: IChatEntitlements = { entitlement: this.context.state.entitlement }; - - private pendingResolveCts = new CancellationTokenSource(); - private didResolveEntitlements = false; - - constructor( - private readonly context: ChatSetupContext, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IAuthenticationService private readonly authenticationService: IAuthenticationService, - @ILogService private readonly logService: ILogService, - @IRequestService private readonly requestService: IRequestService, - @IChatQuotasService private readonly chatQuotasService: IChatQuotasService, - @IDialogService private readonly dialogService: IDialogService, - @IOpenerService private readonly openerService: IOpenerService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(); - - this.registerListeners(); - - this.resolve(); - } - - private registerListeners(): void { - this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.resolve())); - - this._register(this.authenticationService.onDidChangeSessions(e => { - if (e.providerId === ChatSetupRequests.providerId(this.configurationService)) { - this.resolve(); - } - })); - - this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => { - if (e.id === ChatSetupRequests.providerId(this.configurationService)) { - this.resolve(); - } - })); - - this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => { - if (e.id === ChatSetupRequests.providerId(this.configurationService)) { - this.resolve(); - } - })); - - this._register(this.context.onDidChange(() => { - if (!this.context.state.installed || this.context.state.entitlement === ChatEntitlement.Unknown) { - // When the extension is not installed or the user is not entitled - // make sure to clear quotas so that any indicators are also gone - this.state = { entitlement: this.state.entitlement, quotas: undefined }; - this.chatQuotasService.clearQuotas(); - } - })); - } - - private async resolve(): Promise { - this.pendingResolveCts.dispose(true); - const cts = this.pendingResolveCts = new CancellationTokenSource(); - - const session = await this.findMatchingProviderSession(cts.token); - if (cts.token.isCancellationRequested) { - return; - } - - // Immediately signal whether we have a session or not - let state: IChatEntitlements | undefined = undefined; - if (session) { - // Do not overwrite any state we have already - if (this.state.entitlement === ChatEntitlement.Unknown) { - state = { entitlement: ChatEntitlement.Unresolved }; - } - } else { - this.didResolveEntitlements = false; // reset so that we resolve entitlements fresh when signed in again - state = { entitlement: ChatEntitlement.Unknown }; - } - if (state) { - this.update(state); - } - - if (session && !this.didResolveEntitlements) { - // Afterwards resolve entitlement with a network request - // but only unless it was not already resolved before. - await this.resolveEntitlement(session, cts.token); - } - } - - private async findMatchingProviderSession(token: CancellationToken): Promise { - const sessions = await this.doGetSessions(ChatSetupRequests.providerId(this.configurationService)); - if (token.isCancellationRequested) { - return undefined; - } - - for (const session of sessions) { - for (const scopes of defaultChat.providerScopes) { - if (this.scopesMatch(session.scopes, scopes)) { - return session; - } - } - } - - return undefined; - } - - private async doGetSessions(providerId: string): Promise { - try { - return await this.authenticationService.getSessions(providerId); - } catch (error) { - // ignore - errors can throw if a provider is not registered - } - - return []; - } - - private scopesMatch(scopes: ReadonlyArray, expectedScopes: string[]): boolean { - return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope)); - } - - private async resolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { - const entitlements = await this.doResolveEntitlement(session, token); - if (typeof entitlements?.entitlement === 'number' && !token.isCancellationRequested) { - this.didResolveEntitlements = true; - this.update(entitlements); - } - - return entitlements; - } - - private async doResolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { - if (ChatSetupRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) { - this.logService.trace('[chat setup] entitlement: enterprise provider, assuming Pro'); - return { entitlement: ChatEntitlement.Pro }; - } - - if (token.isCancellationRequested) { - return undefined; - } - - const response = await this.request(defaultChat.entitlementUrl, 'GET', undefined, session, token); - if (token.isCancellationRequested) { - return undefined; - } - - if (!response) { - this.logService.trace('[chat setup] entitlement: no response'); - return { entitlement: ChatEntitlement.Unresolved }; - } - - if (response.res.statusCode && response.res.statusCode !== 200) { - this.logService.trace(`[chat setup] entitlement: unexpected status code ${response.res.statusCode}`); - return { entitlement: ChatEntitlement.Unresolved }; - } - - let responseText: string | null = null; - try { - responseText = await asText(response); - } catch (error) { - // ignore - handled below - } - if (token.isCancellationRequested) { - return undefined; - } - - if (!responseText) { - this.logService.trace('[chat setup] entitlement: response has no content'); - return { entitlement: ChatEntitlement.Unresolved }; - } - - let entitlementsResponse: IEntitlementsResponse; - try { - entitlementsResponse = JSON.parse(responseText); - this.logService.trace(`[chat setup] entitlement: parsed result is ${JSON.stringify(entitlementsResponse)}`); - } catch (err) { - this.logService.trace(`[chat setup] entitlement: error parsing response (${err})`); - return { entitlement: ChatEntitlement.Unresolved }; - } - - let entitlement: ChatEntitlement; - if (entitlementsResponse.access_type_sku === 'free_limited_copilot') { - entitlement = ChatEntitlement.Limited; - } else if (entitlementsResponse.can_signup_for_limited) { - entitlement = ChatEntitlement.Available; - } else if (entitlementsResponse.chat_enabled) { - entitlement = ChatEntitlement.Pro; - } else { - entitlement = ChatEntitlement.Unavailable; - } - - const entitlements: IChatEntitlements = { - entitlement, - quotas: { - chatTotal: entitlementsResponse.monthly_quotas?.chat, - completionsTotal: entitlementsResponse.monthly_quotas?.completions, - chatRemaining: entitlementsResponse.limited_user_quotas?.chat, - completionsRemaining: entitlementsResponse.limited_user_quotas?.completions, - resetDate: entitlementsResponse.limited_user_reset_date - } - }; - - this.logService.trace(`[chat setup] entitlement: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`); - this.telemetryService.publicLog2('chatInstallEntitlement', { - entitlement: entitlements.entitlement, - tid: entitlementsResponse.analytics_tracking_id, - quotaChat: entitlementsResponse.limited_user_quotas?.chat, - quotaCompletions: entitlementsResponse.limited_user_quotas?.completions, - quotaResetDate: entitlementsResponse.limited_user_reset_date - }); - - return entitlements; - } - - private async request(url: string, type: 'GET', body: undefined, session: AuthenticationSession, token: CancellationToken): Promise; - private async request(url: string, type: 'POST', body: object, session: AuthenticationSession, token: CancellationToken): Promise; - private async request(url: string, type: 'GET' | 'POST', body: object | undefined, session: AuthenticationSession, token: CancellationToken): Promise { - try { - return await this.requestService.request({ - type, - url, - data: type === 'POST' ? JSON.stringify(body) : undefined, - disableCache: true, - headers: { - 'Authorization': `Bearer ${session.accessToken}` - } - }, token); - } catch (error) { - this.logService.error(`[chat setup] request: error ${error}`); - - return undefined; - } - } - - private update(state: IChatEntitlements): void { - this.state = state; - - this.context.update({ entitlement: this.state.entitlement }); - - if (state.quotas) { - this.chatQuotasService.acceptQuotas({ - chatQuotaExceeded: typeof state.quotas.chatRemaining === 'number' ? state.quotas.chatRemaining <= 0 : false, - completionsQuotaExceeded: typeof state.quotas.completionsRemaining === 'number' ? state.quotas.completionsRemaining <= 0 : false, - quotaResetDate: state.quotas.resetDate ? new Date(state.quotas.resetDate) : undefined, - chatTotal: state.quotas.chatTotal, - completionsTotal: state.quotas.completionsTotal, - chatRemaining: state.quotas.chatRemaining, - completionsRemaining: state.quotas.completionsRemaining - }); - } - } - - async forceResolveEntitlement(session: AuthenticationSession | undefined, token = CancellationToken.None): Promise { - if (!session) { - session = await this.findMatchingProviderSession(token); - } - - if (!session) { - return undefined; - } - - return this.resolveEntitlement(session, token); - } - - async signUpLimited(session: AuthenticationSession): Promise { - const body = { - restricted_telemetry: this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? 'disabled' : 'enabled', - public_code_suggestions: 'enabled' - }; - - const response = await this.request(defaultChat.entitlementSignupLimitedUrl, 'POST', body, session, CancellationToken.None); - if (!response) { - const retry = await this.onUnknownSignUpError(localize('signUpNoResponseError', "No response received."), '[chat setup] sign-up: no response'); - return retry ? this.signUpLimited(session) : { errorCode: 1 }; - } - - if (response.res.statusCode && response.res.statusCode !== 200) { - if (response.res.statusCode === 422) { - try { - const responseText = await asText(response); - if (responseText) { - const responseError: { message: string } = JSON.parse(responseText); - if (typeof responseError.message === 'string' && responseError.message) { - this.onUnprocessableSignUpError(`[chat setup] sign-up: unprocessable entity (${responseError.message})`, responseError.message); - return { errorCode: response.res.statusCode }; - } - } - } catch (error) { - // ignore - handled below - } - } - const retry = await this.onUnknownSignUpError(localize('signUpUnexpectedStatusError', "Unexpected status code {0}.", response.res.statusCode), `[chat setup] sign-up: unexpected status code ${response.res.statusCode}`); - return retry ? this.signUpLimited(session) : { errorCode: response.res.statusCode }; - } - - let responseText: string | null = null; - try { - responseText = await asText(response); - } catch (error) { - // ignore - handled below - } - - if (!responseText) { - const retry = await this.onUnknownSignUpError(localize('signUpNoResponseContentsError', "Response has no contents."), '[chat setup] sign-up: response has no content'); - return retry ? this.signUpLimited(session) : { errorCode: 2 }; - } - - let parsedResult: { subscribed: boolean } | undefined = undefined; - try { - parsedResult = JSON.parse(responseText); - this.logService.trace(`[chat setup] sign-up: response is ${responseText}`); - } catch (err) { - const retry = await this.onUnknownSignUpError(localize('signUpInvalidResponseError', "Invalid response contents."), `[chat setup] sign-up: error parsing response (${err})`); - return retry ? this.signUpLimited(session) : { errorCode: 3 }; - } - - // We have made it this far, so the user either did sign-up or was signed-up already. - // That is, because the endpoint throws in all other case according to Patrick. - this.update({ entitlement: ChatEntitlement.Limited }); - - return Boolean(parsedResult?.subscribed); - } - - private async onUnknownSignUpError(detail: string, logMessage: string): Promise { - this.logService.error(logMessage); - - const { confirmed } = await this.dialogService.confirm({ - type: Severity.Error, - message: localize('unknownSignUpError', "An error occurred while signing up for Copilot Free. Would you like to try again?"), - detail, - primaryButton: localize('retry', "Retry") - }); - - return confirmed; - } - - private onUnprocessableSignUpError(logMessage: string, logDetails: string): void { - this.logService.error(logMessage); - - this.dialogService.prompt({ - type: Severity.Error, - message: localize('unprocessableSignUpError', "An error occurred while signing up for Copilot Free."), - detail: logDetails, - buttons: [ - { - label: localize('ok', "OK"), - run: () => { /* noop */ } - }, - { - label: localize('learnMore', "Learn More"), - run: () => this.openerService.open(URI.parse(defaultChat.upgradePlanUrl)) - } - ] - }); - } - - override dispose(): void { - this.pendingResolveCts.dispose(true); - - super.dispose(); - } } //#endregion @@ -784,11 +579,13 @@ type InstallChatClassification = { installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' }; installDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration it took to install the extension.' }; signUpErrorCode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The error code in case of an error signing up.' }; + setupFromDialog: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setup was triggered from the dialog or not.' }; }; type InstallChatEvent = { installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn' | 'failedSignUp' | 'failedNotTrusted' | 'failedNoSession'; installDuration: number; signUpErrorCode: number | undefined; + setupFromDialog: boolean; }; enum ChatSetupStep { @@ -808,11 +605,10 @@ class ChatSetupController extends Disposable { private willShutdown = false; constructor( - private readonly context: ChatSetupContext, - private readonly requests: ChatSetupRequests, + private readonly context: ChatEntitlementContext, + private readonly requests: ChatEntitlementRequests, @ITelemetryService private readonly telemetryService: ITelemetryService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, - @IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService, @IViewsService private readonly viewsService: IViewsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IProductService private readonly productService: IProductService, @@ -846,7 +642,7 @@ class ChatSetupController extends Disposable { this._onDidChange.fire(); } - async setup(options?: { forceSignIn: boolean }): Promise { + async setup(options?: { forceSignIn?: boolean; notificationProgress?: boolean }): Promise { const watch = new StopWatch(false); const title = localize('setupChatProgress', "Getting Copilot ready..."); const badge = this.activityService.showViewContainerActivity(preferCopilotEditsView(this.viewsService) ? CHAT_EDITING_SIDEBAR_PANEL_ID : CHAT_SIDEBAR_PANEL_ID, { @@ -854,8 +650,8 @@ class ChatSetupController extends Disposable { }); try { - await this.progressService.withProgress({ - location: ProgressLocation.Window, + return await this.progressService.withProgress({ + location: options?.notificationProgress ? ProgressLocation.Notification : ProgressLocation.Window, command: CHAT_SETUP_ACTION_ID, title, }, () => this.doSetup(options?.forceSignIn ?? false, watch)); @@ -864,12 +660,14 @@ class ChatSetupController extends Disposable { } } - private async doSetup(forceSignIn: boolean, watch: StopWatch): Promise { + private async doSetup(forceSignIn: boolean, watch: StopWatch): Promise { this.context.suspend(); // reduces flicker let focusChatInput = false; + let success = false; try { - const providerId = ChatSetupRequests.providerId(this.configurationService); + const setupFromDialog = Boolean(this.configurationService.getValue('chat.experimental.setupFromDialog')); + const providerId = ChatEntitlementRequests.providerId(this.configurationService); let session: AuthenticationSession | undefined; let entitlement: ChatEntitlement | undefined; @@ -878,8 +676,8 @@ class ChatSetupController extends Disposable { this.setStep(ChatSetupStep.SigningIn); const result = await this.signIn(providerId); if (!result.session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined }); - return; + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', installDuration: watch.elapsed(), signUpErrorCode: undefined, setupFromDialog }); + return false; } session = result.session; @@ -890,15 +688,15 @@ class ChatSetupController extends Disposable { message: localize('copilotWorkspaceTrust', "Copilot is currently only supported in trusted workspaces.") }); if (!trusted) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: watch.elapsed(), signUpErrorCode: undefined }); - return; + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotTrusted', installDuration: watch.elapsed(), signUpErrorCode: undefined, setupFromDialog }); + return false; } const activeElement = getActiveElement(); // Install this.setStep(ChatSetupStep.Installing); - await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch); + success = await this.install(session, entitlement ?? this.context.state.entitlement, providerId, watch); const currentActiveElement = getActiveElement(); focusChatInput = activeElement === currentActiveElement || currentActiveElement === mainWindow.document.body; @@ -910,20 +708,17 @@ class ChatSetupController extends Disposable { if (focusChatInput) { (await showCopilotView(this.viewsService, this.layoutService))?.focusInput(); } + + return success; } private async signIn(providerId: string): Promise<{ session: AuthenticationSession | undefined; entitlement: ChatEntitlement | undefined }> { let session: AuthenticationSession | undefined; - let entitlements: IChatEntitlements | undefined; + let entitlements; try { showCopilotView(this.viewsService, this.layoutService); - session = await this.authenticationService.createSession(providerId, defaultChat.providerScopes[0]); - - this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account); - this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, providerId, session.account); - - entitlements = await this.requests.forceResolveEntitlement(session); + ({ session, entitlements } = await this.requests.signIn()); } catch (e) { this.logService.error(`[chat setup] signIn: error ${e}`); } @@ -931,7 +726,7 @@ class ChatSetupController extends Disposable { if (!session && !this.willShutdown) { const { confirmed } = await this.dialogService.confirm({ type: Severity.Error, - message: localize('unknownSignInError', "Failed to sign in to {0}. Would you like to try again?", ChatSetupRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName), + message: localize('unknownSignInError', "Failed to sign in to {0}. Would you like to try again?", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName), detail: localize('unknownSignInErrorDetail', "You must be signed in to use Copilot."), primaryButton: localize('retry', "Retry") }); @@ -944,9 +739,10 @@ class ChatSetupController extends Disposable { return { session, entitlement: entitlements?.entitlement }; } - private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch,): Promise { + private async install(session: AuthenticationSession | undefined, entitlement: ChatEntitlement, providerId: string, watch: StopWatch,): Promise { const wasInstalled = this.context.state.installed; let signUpResult: boolean | { errorCode: number } | undefined = undefined; + const setupFromDialog = Boolean(this.configurationService.getValue('chat.experimental.setupFromDialog')); try { showCopilotView(this.viewsService, this.layoutService); @@ -964,26 +760,26 @@ class ChatSetupController extends Disposable { } if (!session) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined }); - return; // unexpected + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNoSession', installDuration: watch.elapsed(), signUpErrorCode: undefined, setupFromDialog }); + return false; // unexpected } } signUpResult = await this.requests.signUpLimited(session); if (typeof signUpResult !== 'boolean' /* error */) { - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedSignUp', installDuration: watch.elapsed(), signUpErrorCode: signUpResult.errorCode, setupFromDialog }); } } await this.doInstall(); } catch (error) { this.logService.error(`[chat setup] install: error ${error}`); - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined }); - return; + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: isCancellationError(error) ? 'cancelled' : 'failedInstall', installDuration: watch.elapsed(), signUpErrorCode: undefined, setupFromDialog }); + return false; } - this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined }); + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'installed', installDuration: watch.elapsed(), signUpErrorCode: undefined, setupFromDialog }); if (wasInstalled && signUpResult === true) { refreshTokens(this.commandService); @@ -993,6 +789,8 @@ class ChatSetupController extends Disposable { timeout(5000), // helps prevent flicker with sign-in welcome view Event.toPromise(this.chatAgentService.onDidChangeAgents) // https://github.com/microsoft/vscode-copilot/issues/9274 ]); + + return true; } private async doInstall(): Promise { @@ -1035,7 +833,7 @@ class ChatSetupWelcomeContent extends Disposable { constructor( private readonly controller: ChatSetupController, - private readonly context: ChatSetupContext, + private readonly context: ChatEntitlementContext, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -1053,38 +851,32 @@ class ChatSetupWelcomeContent extends Disposable { // Header { - const header = localize({ key: 'header', comment: ['{Locked="[Copilot]({0})"}'] }, "[Copilot]({0}) is your AI pair programmer.", this.context.state.installed ? 'command:github.copilot.open.walkthrough' : defaultChat.documentationUrl); - this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(header, { isTrusted: true }))).element); + const header = localize({ key: 'header', comment: ['{Locked="[Copilot]({0})"}'] }, "[Copilot]({0}) is your AI pair programmer.", this.context.state.installed ? `command:${defaultChat.walkthroughCommand}` : defaultChat.documentationUrl); + this.element.appendChild($('p', undefined, this._register(markdown.render(new MarkdownString(header, { isTrusted: true }))).element)); - const featuresParent = this.element.appendChild($('div.chat-features-container')); - this.element.appendChild(featuresParent); - - const featuresContainer = this.element.appendChild($('div')); - featuresParent.appendChild(featuresContainer); - - const featureChatContainer = featuresContainer.appendChild($('div.chat-feature-container')); - featureChatContainer.appendChild(renderIcon(Codicon.code)); - - const featureChatLabel = featureChatContainer.appendChild($('span')); - featureChatLabel.textContent = localize('featureChat', "Code faster with Completions"); - - const featureEditsContainer = featuresContainer.appendChild($('div.chat-feature-container')); - featureEditsContainer.appendChild(renderIcon(Codicon.editSession)); - - const featureEditsLabel = featureEditsContainer.appendChild($('span')); - featureEditsLabel.textContent = localize('featureEdits', "Build features with Copilot Edits"); - - const featureExploreContainer = featuresContainer.appendChild($('div.chat-feature-container')); - featureExploreContainer.appendChild(renderIcon(Codicon.commentDiscussion)); - - const featureExploreLabel = featureExploreContainer.appendChild($('span')); - featureExploreLabel.textContent = localize('featureExplore', "Explore your codebase with Chat"); + this.element.appendChild( + $('div.chat-features-container', undefined, + $('div', undefined, + $('div.chat-feature-container', undefined, + renderIcon(Codicon.code), + $('span', undefined, localize('featureChat', "Code faster with Completions")) + ), + $('div.chat-feature-container', undefined, + renderIcon(Codicon.editSession), + $('span', undefined, localize('featureEdits', "Build features with Copilot Edits")) + ), + $('div.chat-feature-container', undefined, + renderIcon(Codicon.commentDiscussion), + $('span', undefined, localize('featureExplore', "Explore your codebase with Chat")) + ) + ) + ) + ); } // Limited SKU const free = localize({ key: 'free', comment: ['{Locked="[]({0})"}'] }, "$(sparkle-filled) We now offer [Copilot for free]({0}).", defaultChat.skusDocumentationUrl); - const freeContainer = this.element.appendChild($('p')); - freeContainer.appendChild(this._register(markdown.render(new MarkdownString(free, { isTrusted: true, supportThemeIcons: true }))).element); + const freeContainer = this.element.appendChild($('p', undefined, this._register(markdown.render(new MarkdownString(free, { isTrusted: true, supportThemeIcons: true }))).element)); // Setup Button const buttonContainer = this.element.appendChild($('p')); @@ -1103,12 +895,11 @@ class ChatSetupWelcomeContent extends Disposable { // Terms const terms = localize({ key: 'terms', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "By continuing, you agree to the [Terms]({0}) and [Privacy Policy]({1}).", defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl); - this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(terms, { isTrusted: true }))).element); + this.element.appendChild($('p', undefined, this._register(markdown.render(new MarkdownString(terms, { isTrusted: true }))).element)); // SKU Settings const settings = localize({ key: 'settings', comment: ['{Locked="["}', '{Locked="]({0})"}', '{Locked="]({1})"}'] }, "Copilot Free and Pro may show [public code]({0}) suggestions and we may use your data for product improvement. You can change these [settings]({1}) at any time.", defaultChat.publicCodeMatchesUrl, defaultChat.manageSettingsUrl); - const settingsContainer = this.element.appendChild($('p')); - settingsContainer.appendChild(this._register(markdown.render(new MarkdownString(settings, { isTrusted: true }))).element); + const settingsContainer = this.element.appendChild($('p', undefined, this._register(markdown.render(new MarkdownString(settings, { isTrusted: true }))).element)); // Update based on model state this._register(Event.runAndSubscribe(this.controller.onDidChange, () => this.update(freeContainer, settingsContainer, button))); @@ -1142,7 +933,7 @@ class ChatSetupWelcomeContent extends Disposable { switch (this.controller.step) { case ChatSetupStep.SigningIn: - buttonLabel = localize('setupChatSignIn', "$(loading~spin) Signing in to {0}...", ChatSetupRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName); + buttonLabel = localize('setupChatSignIn', "$(loading~spin) Signing in to {0}...", ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId ? defaultChat.enterpriseProviderName : defaultChat.providerName); break; case ChatSetupStep.Installing: buttonLabel = localize('setupChatInstalling', "$(loading~spin) Getting Copilot Ready..."); @@ -1156,14 +947,19 @@ class ChatSetupWelcomeContent extends Disposable { button.enabled = this.controller.step === ChatSetupStep.Initial; } - private async setupWithProvider(useEnterpriseProvider: boolean): Promise { + private async setupWithProvider(useEnterpriseProvider: boolean): Promise { const registry = Registry.as(ConfigurationExtensions.Configuration); registry.registerConfiguration({ 'id': 'copilot.setup', 'type': 'object', 'properties': { - [defaultChat.providerSetting]: { - 'type': 'string' + [defaultChat.completionsAdvancedSetting]: { + 'type': 'object', + 'properties': { + 'authProvider': { + 'type': 'string' + } + } }, [defaultChat.providerUriSetting]: { 'type': 'string' @@ -1174,11 +970,25 @@ class ChatSetupWelcomeContent extends Disposable { if (useEnterpriseProvider) { const success = await this.handleEnterpriseInstance(); if (!success) { - return; // not properly configured, abort + return false; // not properly configured, abort } - await this.configurationService.updateValue(defaultChat.providerSetting, defaultChat.enterpriseProviderId, ConfigurationTarget.USER); + } + + let existingAdvancedSetting = this.configurationService.inspect(defaultChat.completionsAdvancedSetting).user?.value; + if (!isObject(existingAdvancedSetting)) { + existingAdvancedSetting = {}; + } + + if (useEnterpriseProvider) { + await this.configurationService.updateValue(`${defaultChat.completionsAdvancedSetting}`, { + ...existingAdvancedSetting, + 'authProvider': defaultChat.enterpriseProviderId + }, ConfigurationTarget.USER); } else { - await this.configurationService.updateValue(defaultChat.providerSetting, undefined, ConfigurationTarget.USER); + await this.configurationService.updateValue(`${defaultChat.completionsAdvancedSetting}`, Object.keys(existingAdvancedSetting).length > 0 ? { + ...existingAdvancedSetting, + 'authProvider': undefined + } : undefined, ConfigurationTarget.USER); await this.configurationService.updateValue(defaultChat.providerUriSetting, undefined, ConfigurationTarget.USER); } @@ -1186,35 +996,36 @@ class ChatSetupWelcomeContent extends Disposable { } private async handleEnterpriseInstance(): Promise { + const domainRegEx = /^[a-zA-Z\-_]+$/; + const fullUriRegEx = /^(https:\/\/)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.ghe\.com\/?$/; + const uri = this.configurationService.getValue(defaultChat.providerUriSetting); - if (uri) { - return true; // already setup + if (typeof uri === 'string' && fullUriRegEx.test(uri)) { + return true; // already setup with a valid URI } let isSingleWord = false; const result = await this.quickInputService.input({ prompt: localize('enterpriseInstance', "What is your {0} instance?", defaultChat.enterpriseProviderName), placeHolder: localize('enterpriseInstancePlaceholder', 'i.e. "octocat" or "https://octocat.ghe.com"...'), + value: uri, validateInput: async value => { isSingleWord = false; if (!value) { return undefined; } - if (/^[a-zA-Z\-_]+$/.test(value)) { + if (domainRegEx.test(value)) { isSingleWord = true; return { content: localize('willResolveTo', "Will resolve to {0}", `https://${value}.ghe.com`), severity: Severity.Info }; - } else { - const regex = /^(https:\/\/)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.ghe\.com\/?$/; - if (!regex.test(value)) { - return { - content: localize('invalidEnterpriseInstance', 'Please enter a valid {0} instance (i.e. "octocat" or "https://octocat.ghe.com")', defaultChat.enterpriseProviderName), - severity: Severity.Error - }; - } + } if (!fullUriRegEx.test(value)) { + return { + content: localize('invalidEnterpriseInstance', 'Please enter a valid {0} instance (i.e. "octocat" or "https://octocat.ghe.com")', defaultChat.enterpriseProviderName), + severity: Severity.Error + }; } return undefined; @@ -1254,141 +1065,8 @@ class ChatSetupWelcomeContent extends Disposable { //#endregion -//#region Context - -interface IChatSetupContextState { - entitlement: ChatEntitlement; - hidden?: boolean; - installed?: boolean; - registered?: boolean; -} - -class ChatSetupContext extends Disposable { - - private static readonly CHAT_SETUP_CONTEXT_STORAGE_KEY = 'chat.setupContext'; - - private readonly canSignUpContextKey = ChatContextKeys.Setup.canSignUp.bindTo(this.contextKeyService); - private readonly signedOutContextKey = ChatContextKeys.Setup.signedOut.bindTo(this.contextKeyService); - private readonly limitedContextKey = ChatContextKeys.Setup.limited.bindTo(this.contextKeyService); - private readonly proContextKey = ChatContextKeys.Setup.pro.bindTo(this.contextKeyService); - private readonly hiddenContext = ChatContextKeys.Setup.hidden.bindTo(this.contextKeyService); - private readonly installedContext = ChatContextKeys.Setup.installed.bindTo(this.contextKeyService); - - private _state: IChatSetupContextState = this.storageService.getObject(ChatSetupContext.CHAT_SETUP_CONTEXT_STORAGE_KEY, StorageScope.PROFILE) ?? { entitlement: ChatEntitlement.Unknown }; - private suspendedState: IChatSetupContextState | undefined = undefined; - get state(): IChatSetupContextState { - return this.suspendedState ?? this._state; - } - - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; - - private updateBarrier: Barrier | undefined = undefined; - - constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IStorageService private readonly storageService: IStorageService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @ILogService private readonly logService: ILogService, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - ) { - super(); - - this.checkExtensionInstallation(); - this.updateContextSync(); - } - - private async checkExtensionInstallation(): Promise { - - // Await extensions to be ready to be queried - await this.extensionsWorkbenchService.queryLocal(); - - // Listen to change and process extensions once - this._register(Event.runAndSubscribe(this.extensionsWorkbenchService.onChange, e => { - if (e && !ExtensionIdentifier.equals(e.identifier.id, defaultChat.extensionId)) { - return; // unrelated event - } - - const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); - this.update({ installed: !!defaultChatExtension?.local && this.extensionEnablementService.isEnabled(defaultChatExtension.local) }); - })); - } - - update(context: { installed: boolean }): Promise; - update(context: { hidden: boolean }): Promise; - update(context: { entitlement: ChatEntitlement }): Promise; - update(context: { installed?: boolean; hidden?: boolean; entitlement?: ChatEntitlement }): Promise { - this.logService.trace(`[chat setup] update(): ${JSON.stringify(context)}`); - - if (typeof context.installed === 'boolean') { - this._state.installed = context.installed; - - if (context.installed) { - context.hidden = false; // allows to fallback to setup view if the extension is uninstalled - } - } - - if (typeof context.hidden === 'boolean') { - this._state.hidden = context.hidden; - } - - if (typeof context.entitlement === 'number') { - this._state.entitlement = context.entitlement; - - if (this._state.entitlement === ChatEntitlement.Limited || this._state.entitlement === ChatEntitlement.Pro) { - this._state.registered = true; // remember that the user did register to improve setup screen - } else if (this._state.entitlement === ChatEntitlement.Available) { - this._state.registered = false; // only restore when signed-in user can sign-up for limited - } - } - - this.storageService.store(ChatSetupContext.CHAT_SETUP_CONTEXT_STORAGE_KEY, this._state, StorageScope.PROFILE, StorageTarget.MACHINE); - - return this.updateContext(); - } - - private async updateContext(): Promise { - await this.updateBarrier?.wait(); - - this.updateContextSync(); - } - - private updateContextSync(): void { - this.logService.trace(`[chat setup] updateContext(): ${JSON.stringify(this._state)}`); - - if (!this._state.hidden && !this._state.installed) { - // this is ugly but fixes flicker from a previous chat install - this.storageService.remove('chat.welcomeMessageContent.panel', StorageScope.APPLICATION); - this.storageService.remove('interactive.sessions', this.workspaceContextService.getWorkspace().folders.length ? StorageScope.WORKSPACE : StorageScope.APPLICATION); - } - - this.signedOutContextKey.set(this._state.entitlement === ChatEntitlement.Unknown); - this.canSignUpContextKey.set(this._state.entitlement === ChatEntitlement.Available); - this.limitedContextKey.set(this._state.entitlement === ChatEntitlement.Limited); - this.proContextKey.set(this._state.entitlement === ChatEntitlement.Pro); - this.hiddenContext.set(!!this._state.hidden); - this.installedContext.set(!!this._state.installed); - - this._onDidChange.fire(); - } - - suspend(): void { - this.suspendedState = { ...this._state }; - this.updateBarrier = new Barrier(); - } - - resume(): void { - this.suspendedState = undefined; - this.updateBarrier?.open(); - this.updateBarrier = undefined; - } -} - -//#endregion - function refreshTokens(commandService: ICommandService): void { // ugly, but we need to signal to the extension that entitlements changed - commandService.executeCommand('github.copilot.signIn'); - commandService.executeCommand('github.copilot.refreshToken'); + commandService.executeCommand(defaultChat.completionsRefreshTokenCommand); + commandService.executeCommand(defaultChat.chatRefreshTokenCommand); } diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index f4e9d75cc1c..6d1030537c4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -6,48 +6,103 @@ import './media/chatStatus.css'; import { safeIntl } from '../../../../base/common/date.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { language, OS } from '../../../../base/common/platform.js'; +import { language } from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { IProductService } from '../../../../platform/product/common/productService.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; -import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, TooltipContent } from '../../../services/statusbar/browser/statusbar.js'; -import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { IChatQuotasService } from '../common/chatQuotasService.js'; -import { quotaToButtonMessage, OPEN_CHAT_QUOTA_EXCEEDED_DIALOG, CHAT_SETUP_ACTION_LABEL, TOGGLE_CHAT_ACTION_ID, CHAT_OPEN_ACTION_ID } from './actions/chatActions.js'; -import { $, addDisposableListener, append, EventType } from '../../../../base/browser/dom.js'; -import { IChatEntitlementsService } from '../common/chatEntitlementsService.js'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind } from '../../../services/statusbar/browser/statusbar.js'; +import { $, addDisposableListener, append, clearNode, EventHelper, EventType } from '../../../../base/browser/dom.js'; +import { ChatEntitlement, ChatEntitlementService, ChatSentiment, IChatEntitlementService } from '../common/chatEntitlementService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; -import { defaultCheckboxStyles, defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { Command } from '../../../../editor/common/languages.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { Lazy } from '../../../../base/common/lazy.js'; +import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { Color } from '../../../../base/common/color.js'; +import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import product from '../../../../platform/product/common/product.js'; +import { isObject } from '../../../../base/common/types.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; + +//#region --- colors + +const gaugeBackground = registerColor('gauge.background', { + dark: inputValidationInfoBorder, + light: inputValidationInfoBorder, + hcDark: contrastBorder, + hcLight: contrastBorder +}, localize('gaugeBackground', "Gauge background color.")); + +registerColor('gauge.foreground', { + dark: transparent(gaugeBackground, 0.3), + light: transparent(gaugeBackground, 0.3), + hcDark: Color.white, + hcLight: Color.white +}, localize('gaugeForeground', "Gauge foreground color.")); + +registerColor('gauge.border', { + dark: null, + light: null, + hcDark: contrastBorder, + hcLight: contrastBorder +}, localize('gaugeBorder', "Gauge border color.")); + +const gaugeWarningBackground = registerColor('gauge.warningBackground', { + dark: inputValidationWarningBorder, + light: inputValidationWarningBorder, + hcDark: contrastBorder, + hcLight: contrastBorder +}, localize('gaugeWarningBackground', "Gauge warning background color.")); + +registerColor('gauge.warningForeground', { + dark: transparent(gaugeWarningBackground, 0.3), + light: transparent(gaugeWarningBackground, 0.3), + hcDark: Color.white, + hcLight: Color.white +}, localize('gaugeWarningForeground', "Gauge warning foreground color.")); + +const gaugeErrorBackground = registerColor('gauge.errorBackground', { + dark: inputValidationErrorBorder, + light: inputValidationErrorBorder, + hcDark: contrastBorder, + hcLight: contrastBorder +}, localize('gaugeErrorBackground', "Gauge error background color.")); + +registerColor('gauge.errorForeground', { + dark: transparent(gaugeErrorBackground, 0.3), + light: transparent(gaugeErrorBackground, 0.3), + hcDark: Color.white, + hcLight: Color.white +}, localize('gaugeErrorForeground', "Gauge error foreground color.")); + +//#endregion + +const defaultChat = { + extensionId: product.defaultChatAgent?.extensionId ?? '', + completionsEnablementSetting: product.defaultChatAgent?.completionsEnablementSetting ?? '', + nextEditSuggestionsSetting: product.defaultChatAgent?.nextEditSuggestionsSetting ?? '' +}; export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution { static readonly ID = 'chat.statusBarEntry'; - private readonly treatment = this.assignmentService.getTreatment('config.chat.experimental.statusIndicator.enabled'); //TODO@bpasero remove this experiment eventually + private static readonly SETTING = 'chat.experimental.statusIndicator.enabled'; private entry: IStatusbarEntryAccessor | undefined = undefined; - private readonly entryDisposables = this._register(new MutableDisposable()); - private dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' }); + private dashboard = new Lazy(() => this.instantiationService.createInstance(ChatStatusDashboard)); constructor( @IStatusbarService private readonly statusbarService: IStatusbarService, - @IChatQuotasService private readonly chatQuotasService: IChatQuotasService, - @IChatEntitlementsService private readonly chatEntitlementsService: IChatEntitlementsService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IWorkbenchAssignmentService private readonly assignmentService: IWorkbenchAssignmentService, - @IProductService private readonly productService: IProductService, - @IKeybindingService private readonly keybindingService: IKeybindingService, + @IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService, @IConfigurationService private readonly configurationService: IConfigurationService, - @ICommandService private readonly commandService: ICommandService + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); @@ -56,150 +111,212 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu } private async create(): Promise { - let enabled = false; - if (this.productService.quality === 'stable') { - enabled = (await this.treatment) === true; + const hidden = this.chatEntitlementService.sentiment === ChatSentiment.Disabled; + const disabled = this.configurationService.getValue(ChatStatusBarEntry.SETTING) === false; + + if (!hidden && !disabled) { + this.entry ||= this.statusbarService.addEntry(this.getEntryProps(), ChatStatusBarEntry.ID, StatusbarAlignment.RIGHT, { location: { id: 'status.editor.mode', priority: 100.1 }, alignment: StatusbarAlignment.RIGHT }); + + // TODO@bpasero: remove this eventually + const completionsStatusId = `${defaultChat.extensionId}.status`; + this.statusbarService.updateEntryVisibility(completionsStatusId, false); + this.statusbarService.overrideEntry(completionsStatusId, { name: localize('codeCompletionsStatus', "Copilot Code Completions"), text: localize('codeCompletionsStatusText', "$(copilot) Completions") }); } else { - enabled = true; + this.entry?.dispose(); + this.entry = undefined; } - - if (!enabled) { - return; - } - - this.entry = this._register(this.statusbarService.addEntry(this.getEntryProps(), ChatStatusBarEntry.ID, StatusbarAlignment.RIGHT, Number.NEGATIVE_INFINITY /* the end of the right hand side */)); } private registerListeners(): void { - const contextKeysSet = new Set([ - ChatContextKeys.Setup.limited.key, - ChatContextKeys.Setup.installed.key, - ChatContextKeys.Setup.canSignUp.key, - ChatContextKeys.Setup.signedOut.key - ]); - this._register(this.contextKeyService.onDidChangeContext(e => { - if (!this.entry) { - return; - } - - if (e.affectsSome(contextKeysSet)) { - this.entry.update(this.getEntryProps()); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(ChatStatusBarEntry.SETTING)) { + this.create(); } })); - this._register(this.chatQuotasService.onDidChangeQuotaExceeded(() => this.entry?.update(this.getEntryProps()))); + this._register(this.chatEntitlementService.onDidChangeQuotaExceeded(() => this.entry?.update(this.getEntryProps()))); + this._register(this.chatEntitlementService.onDidChangeSentiment(() => this.entry?.update(this.getEntryProps()))); + this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.entry?.update(this.getEntryProps()))); } private getEntryProps(): IStatusbarEntry { - const disposables = new DisposableStore(); - this.entryDisposables.value = disposables; - let text = '$(copilot)'; let ariaLabel = localize('chatStatus', "Copilot Status"); - let command: string | Command = TOGGLE_CHAT_ACTION_ID; - let tooltip: TooltipContent = localize('openChat', "Open Chat ({0})", this.keybindingService.lookupKeybinding(command)?.getLabel() ?? ''); + let kind: StatusbarEntryKind | undefined; - // Quota Exceeded - const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatQuotasService.quotas; - if (chatQuotaExceeded || completionsQuotaExceeded) { - let quotaWarning: string; - if (chatQuotaExceeded && !completionsQuotaExceeded) { - quotaWarning = localize('chatQuotaExceededStatus', "Chat limit reached"); - } else if (completionsQuotaExceeded && !chatQuotaExceeded) { - quotaWarning = localize('completionsQuotaExceededStatus', "Completions limit reached"); - } else { - quotaWarning = localize('chatAndCompletionsQuotaExceededStatus', "Limit reached"); + if (!isNewUser(this.chatEntitlementService)) { + const { chatQuotaExceeded, completionsQuotaExceeded } = this.chatEntitlementService.quotas; + + // Signed out + if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { + const signedOutWarning = localize('notSignedIntoCopilot', "Signed out"); + + text = `$(copilot-not-connected) ${signedOutWarning}`; + ariaLabel = signedOutWarning; + kind = 'prominent'; } - text = `$(copilot-warning) ${quotaWarning}`; - ariaLabel = quotaWarning; - command = OPEN_CHAT_QUOTA_EXCEEDED_DIALOG; - tooltip = quotaToButtonMessage({ chatQuotaExceeded, completionsQuotaExceeded }); - } + // Quota Exceeded + else if (chatQuotaExceeded || completionsQuotaExceeded) { + let quotaWarning: string; + if (chatQuotaExceeded && !completionsQuotaExceeded) { + quotaWarning = localize('chatQuotaExceededStatus', "Chat limit reached"); + } else if (completionsQuotaExceeded && !chatQuotaExceeded) { + quotaWarning = localize('completionsQuotaExceededStatus', "Completions limit reached"); + } else { + quotaWarning = localize('chatAndCompletionsQuotaExceededStatus', "Limit reached"); + } - // Copilot Not Installed - else if ( - this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.installed.key) === false || - this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.canSignUp.key) === true - ) { - tooltip = CHAT_SETUP_ACTION_LABEL.value; - } - - // Signed out - else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.signedOut.key) === true) { - text = '$(copilot-not-connected)'; - ariaLabel = localize('signInToUseCopilot', "Sign in to Use Copilot..."); - tooltip = localize('signInToUseCopilot', "Sign in to Use Copilot..."); - } - - // Copilot Limited User - else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.limited.key) === true) { - tooltip = () => { - const container = $('div.chat-status-bar-entry-tooltip'); - - // Quota Indicator - const { chatTotal, chatRemaining, completionsTotal, completionsRemaining, quotaResetDate } = this.chatQuotasService.quotas; - - container.appendChild($('div', undefined, localize('limitTitle', "You are currently using Copilot Free:"))); - - const chatQuotaIndicator = this.createQuotaIndicator(container, chatTotal, chatRemaining, localize('chatsLabel', "Chats Used")); - const completionsQuotaIndicator = this.createQuotaIndicator(container, completionsTotal, completionsRemaining, localize('completionsLabel', "Completions Used")); - - this.chatEntitlementsService.resolve(CancellationToken.None).then(() => { - const { chatTotal, chatRemaining, completionsTotal, completionsRemaining } = this.chatQuotasService.quotas; - - chatQuotaIndicator(chatTotal, chatRemaining); - completionsQuotaIndicator(completionsTotal, completionsRemaining); - }); - - container.appendChild($('div', undefined, localize('limitQuota', "Usage will reset on {0}.", this.dateFormatter.format(quotaResetDate)))); - - // Settings - container.appendChild(document.createElement('hr')); - this.createSettings(container, disposables); - - // Shortcuts - container.appendChild(document.createElement('hr')); - this.createShortcuts(container, disposables); - - return container; - }; - command = ShowTooltipCommand; - } - - // Any other User - else { - tooltip = () => { - const container = $('div.chat-status-bar-entry-tooltip'); - - // Settings - this.createSettings(container, disposables); - - // Shortcuts - container.appendChild($('hr')); - this.createShortcuts(container, disposables); - - return container; - }; - command = ShowTooltipCommand; + text = `$(copilot-warning) ${quotaWarning}`; + ariaLabel = quotaWarning; + kind = 'prominent'; + } } return { name: localize('chatStatus', "Copilot Status"), text, ariaLabel, - command, + command: ShowTooltipCommand, showInAllWindows: true, - kind: 'copilot', - tooltip + kind, + tooltip: { element: token => this.dashboard.value.show(token) } }; } + override dispose(): void { + super.dispose(); + + this.entry?.dispose(); + this.entry = undefined; + } +} + +function isNewUser(chatEntitlementService: IChatEntitlementService): boolean { + return chatEntitlementService.sentiment !== ChatSentiment.Installed || // copilot not installed + chatEntitlementService.entitlement === ChatEntitlement.Available; // not yet signed up to copilot +} + +function canUseCopilot(chatEntitlementService: IChatEntitlementService): boolean { + const newUser = isNewUser(chatEntitlementService); + const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown; + const allQuotaReached = chatEntitlementService.quotas.chatQuotaExceeded && chatEntitlementService.quotas.completionsQuotaExceeded; + + return !newUser && !signedOut && !allQuotaReached; +} + +interface ISettingsAccessor { + readSetting: () => boolean; + writeSetting: (value: boolean) => Promise; +} + +class ChatStatusDashboard extends Disposable { + + private readonly element = $('div.chat-status-bar-entry-tooltip'); + + private dateFormatter = new Lazy(() => safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' })); + private readonly entryDisposables = this._register(new MutableDisposable()); + + constructor( + @IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IHoverService private readonly hoverService: IHoverService, + @IEditorService private readonly editorService: IEditorService, + @ILanguageService private readonly languageService: ILanguageService, + @ICommandService private readonly commandService: ICommandService + ) { + super(); + } + + show(token: CancellationToken): HTMLElement { + clearNode(this.element); + + const disposables = this.entryDisposables.value = new DisposableStore(); + disposables.add(token.onCancellationRequested(() => disposables.dispose())); + + let needsSeparator = false; + const addSeparator = (label: string | undefined) => { + if (needsSeparator) { + this.element.appendChild($('hr')); + needsSeparator = false; + } + + if (label) { + this.element.appendChild($('div.header', undefined, label)); + } + + needsSeparator = true; + }; + + // Quota Indicator + if (this.chatEntitlementService.entitlement === ChatEntitlement.Limited) { + const { chatTotal, chatRemaining, completionsTotal, completionsRemaining, quotaResetDate, chatQuotaExceeded, completionsQuotaExceeded } = this.chatEntitlementService.quotas; + + addSeparator(localize('usageTitle', "Copilot Free Usage")); + + const chatQuotaIndicator = this.createQuotaIndicator(this.element, chatTotal, chatRemaining, localize('chatsLabel', "Chat messages")); + const completionsQuotaIndicator = this.createQuotaIndicator(this.element, completionsTotal, completionsRemaining, localize('completionsLabel', "Code completions")); + + this.element.appendChild($('div.description', undefined, localize('limitQuota', "Limits will reset on {0}.", this.dateFormatter.value.format(quotaResetDate)))); + + if (chatQuotaExceeded || completionsQuotaExceeded) { + const upgradePlanButton = disposables.add(new Button(this.element, { ...defaultButtonStyles, secondary: canUseCopilot(this.chatEntitlementService) /* use secondary color when copilot can still be used */ })); + upgradePlanButton.label = localize('upgradeToCopilotPro', "Upgrade to Copilot Pro"); + disposables.add(upgradePlanButton.onDidClick(() => this.runCommandAndClose({ id: 'workbench.action.chat.upgradePlan', args: ['chat-status'] }))); + } + + (async () => { + await this.chatEntitlementService.update(token); + if (token.isCancellationRequested) { + return; + } + + const { chatTotal, chatRemaining, completionsTotal, completionsRemaining } = this.chatEntitlementService.quotas; + + chatQuotaIndicator(chatTotal, chatRemaining); + completionsQuotaIndicator(completionsTotal, completionsRemaining); + })(); + } + + // Settings + { + addSeparator(localize('settingsTitle', "Settings")); + + this.createSettings(this.element, disposables); + } + + // New to Copilot / Signed out + { + const newUser = isNewUser(this.chatEntitlementService); + const signedOut = this.chatEntitlementService.entitlement === ChatEntitlement.Unknown; + if (newUser || signedOut) { + addSeparator(undefined); + + this.element.appendChild($('div.description', undefined, newUser ? localize('activateDescription', "You need to set up Copilot.") : localize('signInDescription', "You need to sign in to use Copilot."))); + + const button = disposables.add(new Button(this.element, { ...defaultButtonStyles })); + button.label = newUser ? localize('activateCopilotButton', "Set Up Copilot") : localize('signInToUseCopilotButton', "Sign In"); + disposables.add(button.onDidClick(() => this.runCommandAndClose(newUser ? { id: 'workbench.action.chat.triggerSetup' } : () => this.chatEntitlementService.requests?.value.signIn()))); + } + } + + return this.element; + } + + private runCommandAndClose(command: { id: string; args?: unknown[] } | Function): void { + if (typeof command === 'function') { + command(); + } else { + this.commandService.executeCommand(command.id, ...(command.args ?? [])); + } + this.hoverService.hideHover(true); + } + private createQuotaIndicator(container: HTMLElement, total: number | undefined, remaining: number | undefined, label: string): (total: number | undefined, remaining: number | undefined) => void { const quotaText = $('span'); const quotaBit = $('div.quota-bit'); - container.appendChild($('div.quota-indicator', undefined, + const quotaIndicator = container.appendChild($('div.quota-indicator', undefined, $('div.quota-label', undefined, $('span', undefined, label), quotaText @@ -210,9 +327,23 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu )); const update = (total: number | undefined, remaining: number | undefined) => { + quotaIndicator.classList.remove('error'); + quotaIndicator.classList.remove('warning'); + if (typeof total === 'number' && typeof remaining === 'number') { - quotaText.textContent = localize('quotaDisplay', "{0} / {1}", total - remaining, total); - quotaBit.style.width = `${((total - remaining) / total) * 100}%`; + let usedPercentage = Math.round(((total - remaining) / total) * 100); + if (total !== remaining && usedPercentage === 0) { + usedPercentage = 1; // indicate minimal usage as 1% + } + + quotaText.textContent = localize('quotaDisplay', "{0}%", usedPercentage); + quotaBit.style.width = `${usedPercentage}%`; + + if (usedPercentage >= 90) { + quotaIndicator.classList.add('error'); + } else if (usedPercentage >= 75) { + quotaIndicator.classList.add('warning'); + } } }; @@ -221,64 +352,131 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu return update; } - private createShortcuts(container: HTMLElement, disposables: DisposableStore): HTMLElement { - const shortcuts = container.appendChild($('div.shortcuts')); + private createSettings(container: HTMLElement, disposables: DisposableStore): HTMLElement { + const modeId = this.editorService.activeTextEditorLanguageId; + const settings = container.appendChild($('div.settings')); - const openChat = { text: localize('shortcuts.chat', "Chat"), id: CHAT_OPEN_ACTION_ID }; - const openCopilotEdits = { text: localize('shortcuts.copilotEdits', "Copilot Edits"), id: 'workbench.action.chat.openEditSession' }; - const inlineChat = { text: localize('shortcuts.inlineChat', "Inline Chat"), id: 'inlineChat.start' }; + // --- Code Completions + { + const globalSetting = append(settings, $('div.setting')); + this.createCodeCompletionsSetting(globalSetting, localize('settings.codeCompletions', "Code Completions (all files)"), '*', disposables); - for (const entry of [openChat, openCopilotEdits, inlineChat]) { - const keys = this.keybindingService.lookupKeybinding(entry.id); - if (!keys) { - continue; - } - - const shortcut = append(shortcuts, $('div.shortcut')); - - const shortcutLabel = append(shortcut, $('span.shortcut-label', undefined, entry.text)); - - const shortcutKey = disposables.add(new KeybindingLabel(shortcut, OS, { ...defaultKeybindingLabelStyles })); - shortcutKey.set(keys); - - for (const element of [shortcutLabel, shortcutKey.element]) { - disposables.add(addDisposableListener(element, EventType.CLICK, e => { - this.commandService.executeCommand(entry.id); - })); + if (modeId) { + const languageSetting = append(settings, $('div.setting')); + this.createCodeCompletionsSetting(languageSetting, localize('settings.codeCompletionsLanguage', "Code Completions ({0})", this.languageService.getLanguageName(modeId) ?? modeId), modeId, disposables); } } - return shortcuts; - } - - private createSettings(container: HTMLElement, disposables: DisposableStore): HTMLElement { - const settings = container.appendChild($('div.settings')); - - const toggleCompletions = { text: localize('settings.toggleCompletions', "Code Completions"), id: 'editor.inlineSuggest.enabled' }; - const toggleNextEditSuggestions = { text: localize('settings.toggleNextEditSuggestions', "Next Edit Suggestions (Preview)"), id: 'github.copilot.nextEditSuggestions.enabled' }; - - for (const entry of [toggleCompletions, toggleNextEditSuggestions]) { - const checked = Boolean(this.configurationService.getValue(entry.id)); - + // --- Next Edit Suggestions + { const setting = append(settings, $('div.setting')); - - const checkbox = disposables.add(new Checkbox(entry.text, checked, defaultCheckboxStyles)); - setting.appendChild(checkbox.domNode); - - const settingLabel = append(setting, $('span.setting-label', undefined, entry.text)); - disposables.add(addDisposableListener(settingLabel, EventType.CLICK, e => { - if (checkbox?.enabled && (e.target as HTMLElement).tagName !== 'A') { - checkbox.checked = !checkbox.checked; - this.configurationService.updateValue(entry.id, checkbox.checked); - checkbox.focus(); - } - })); - - disposables.add(checkbox.onChange(() => { - this.configurationService.updateValue(entry.id, checkbox.checked); - })); + this.createNextEditSuggestionsSetting(setting, localize('settings.nextEditSuggestions', "Next Edit Suggestions"), modeId, this.getCompletionsSettingAccessor(modeId), disposables); } return settings; } + + private createSetting(container: HTMLElement, settingId: string, label: string, accessor: ISettingsAccessor, disposables: DisposableStore): Checkbox { + const checkbox = disposables.add(new Checkbox(label, Boolean(accessor.readSetting()), defaultCheckboxStyles)); + container.appendChild(checkbox.domNode); + + const settingLabel = append(container, $('span.setting-label', undefined, label)); + disposables.add(Gesture.addTarget(settingLabel)); + [EventType.CLICK, TouchEventType.Tap].forEach(eventType => { + disposables.add(addDisposableListener(settingLabel, eventType, e => { + if (checkbox?.enabled) { + EventHelper.stop(e, true); + + checkbox.checked = !checkbox.checked; + accessor.writeSetting(checkbox.checked); + checkbox.focus(); + } + })); + }); + + disposables.add(checkbox.onChange(() => { + accessor.writeSetting(checkbox.checked); + })); + + disposables.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(settingId)) { + checkbox.checked = Boolean(accessor.readSetting()); + } + })); + + if (!canUseCopilot(this.chatEntitlementService)) { + container.classList.add('disabled'); + checkbox.disable(); + } + + return checkbox; + } + + private createCodeCompletionsSetting(container: HTMLElement, label: string, modeId: string | undefined, disposables: DisposableStore): void { + this.createSetting(container, defaultChat.completionsEnablementSetting, label, this.getCompletionsSettingAccessor(modeId), disposables); + } + + private getCompletionsSettingAccessor(modeId = '*'): ISettingsAccessor { + const settingId = defaultChat.completionsEnablementSetting; + + return { + readSetting: () => { + const result = this.configurationService.getValue>(settingId); + if (!isObject(result)) { + return false; + } + + if (typeof result[modeId] !== 'undefined') { + return Boolean(result[modeId]); // go with setting if explicitly defined + } + + return Boolean(result['*']); // fallback to global setting otherwise + }, + writeSetting: (value: boolean) => { + let result = this.configurationService.getValue>(settingId); + if (!isObject(result)) { + result = Object.create(null); + } + + return this.configurationService.updateValue(settingId, { ...result, [modeId]: value }); + } + }; + } + + private createNextEditSuggestionsSetting(container: HTMLElement, label: string, modeId: string | undefined, completionsSettingAccessor: ISettingsAccessor, disposables: DisposableStore): void { + const nesSettingId = defaultChat.nextEditSuggestionsSetting; + const completionsSettingId = defaultChat.completionsEnablementSetting; + + const checkbox = this.createSetting(container, nesSettingId, label, { + readSetting: () => this.configurationService.getValue(nesSettingId, { overrideIdentifier: modeId }), + writeSetting: (value: boolean) => { + const { overrideIdentifiers } = this.configurationService.inspect(nesSettingId, { overrideIdentifier: modeId }); + if (modeId && overrideIdentifiers?.includes(modeId)) { + return this.configurationService.updateValue(nesSettingId, value, { overrideIdentifier: modeId }); + } + + return this.configurationService.updateValue(nesSettingId, value); + } + }, disposables); + + // enablement of NES depends on completions setting + // so we have to update our checkbox state accordingly + + if (!completionsSettingAccessor.readSetting()) { + container.classList.add('disabled'); + checkbox.disable(); + } + + disposables.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(completionsSettingId)) { + if (completionsSettingAccessor.readSetting() && canUseCopilot(this.chatEntitlementService)) { + checkbox.enable(); + container.classList.remove('disabled'); + } else { + checkbox.disable(); + container.classList.add('disabled'); + } + } + })); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index dde6ec6e59f..75e7c2d38e5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -4,14 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce } from '../../../../base/common/arrays.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { Location } from '../../../../editor/common/languages.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { ChatAgentLocation } from '../common/chatAgents.js'; import { IChatRequestVariableData, IChatRequestVariableEntry } from '../common/chatModel.js'; import { ChatRequestDynamicVariablePart, ChatRequestToolPart, IParsedChatRequest } from '../common/chatParserTypes.js'; import { IChatVariablesService, IDynamicVariable } from '../common/chatVariables.js'; +import { ChatConfiguration } from '../common/constants.js'; import { IChatWidgetService, showChatView, showEditsView } from './chat.js'; import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js'; @@ -21,6 +22,7 @@ export class ChatVariablesService implements IChatVariablesService { constructor( @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IViewsService private readonly viewsService: IViewsService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { } @@ -29,10 +31,8 @@ export class ChatVariablesService implements IChatVariablesService { prompt.parts .forEach((part, i) => { - if (part instanceof ChatRequestDynamicVariablePart) { - resolvedVariables[i] = { id: part.id, name: part.referenceText, range: part.range, value: part.data, fullName: part.fullName, icon: part.icon, isFile: part.isFile }; - } else if (part instanceof ChatRequestToolPart) { - resolvedVariables[i] = { id: part.toolId, name: part.toolName, range: part.range, value: undefined, isTool: true, icon: ThemeIcon.isThemeIcon(part.icon) ? part.icon : undefined, fullName: part.displayName }; + if (part instanceof ChatRequestDynamicVariablePart || part instanceof ChatRequestToolPart) { + resolvedVariables[i] = part.toVariableEntry(); } }); @@ -76,7 +76,8 @@ export class ChatVariablesService implements IChatVariablesService { return; } - const widget = location === ChatAgentLocation.EditingSession + const unifiedViewEnabled = !!this.configurationService.getValue(ChatConfiguration.UnifiedChatView); + const widget = location === ChatAgentLocation.EditingSession && !unifiedViewEnabled ? await showEditsView(this.viewsService) : (this.chatWidgetService.lastFocusedWidget ?? await showChatView(this.viewsService)); if (!widget || !widget.viewModel) { @@ -90,5 +91,10 @@ export class ChatVariablesService implements IChatVariablesService { widget.attachmentModel.addFile(uri, range); return; } + + if (key === 'folder' && URI.isUri(value)) { + widget.attachmentModel.addFolder(value); + return; + } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 6e9563085f7..3cf5196fdd4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -25,11 +25,12 @@ import { Memento } from '../../../common/memento.js'; import { SIDE_BAR_FOREGROUND } from '../../../common/theme.js'; import { IViewDescriptorService } from '../../../common/views.js'; import { IChatViewTitleActionContext } from '../common/chatActions.js'; -import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; +import { IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ChatModelInitState, IChatModel } from '../common/chatModel.js'; import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js'; import { IChatService } from '../common/chatService.js'; +import { ChatAgentLocation, ChatMode } from '../common/constants.js'; import { ChatWidget, IChatViewState } from './chatWidget.js'; import { ChatViewWelcomeController, IViewWelcomeDelegate } from './viewsWelcome/chatViewWelcomeController.js'; @@ -76,8 +77,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this._register(this.chatAgentService.onDidChangeAgents(() => { if (this.chatAgentService.getDefaultAgent(this.chatOptions?.location)) { if (!this._widget?.viewModel) { - const sessionId = this.getSessionId(); - const model = sessionId ? this.chatService.getOrRestoreSession(sessionId) : undefined; + const info = this.getTransferredOrPersistedSessionInfo(); + const model = info.sessionId ? this.chatService.getOrRestoreSession(info.sessionId) : undefined; // The widget may be hidden at this point, because welcome views were allowed. Use setVisible to // avoid doing a render while the widget is hidden. This is changing the condition in `shouldShowWelcome` @@ -85,7 +86,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const wasVisible = this._widget.visible; try { this._widget.setVisible(false); - this.updateModel(model); + this.updateModel(model, info.inputValue || info.mode ? { inputState: { chatMode: info.mode }, inputValue: info.inputValue } : undefined); this.defaultParticipantRegistrationFailed = false; this.didUnregisterProvider = false; this._onDidChangeViewWelcomeState.fire(); @@ -144,15 +145,17 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { return !!shouldShow; } - private getSessionId() { - let sessionId: string | undefined; + private getTransferredOrPersistedSessionInfo(): { sessionId?: string; inputValue?: string; mode?: ChatMode } { if (this.chatService.transferredSessionData?.location === this.chatOptions.location) { - sessionId = this.chatService.transferredSessionData.sessionId; - this.viewState.inputValue = this.chatService.transferredSessionData.inputValue; + const sessionId = this.chatService.transferredSessionData.sessionId; + return { + sessionId, + inputValue: this.chatService.transferredSessionData.inputValue, + mode: this.chatService.transferredSessionData.mode + }; } else { - sessionId = this.viewState.sessionId; + return { sessionId: this.viewState.sessionId }; } - return sessionId; } protected override renderBody(parent: HTMLElement): void { @@ -171,21 +174,22 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.chatOptions.location, { viewId: this.id }, { - autoScroll: this.chatOptions.location === ChatAgentLocation.EditingSession, + autoScroll: mode => mode !== ChatMode.Chat, renderFollowups: this.chatOptions.location === ChatAgentLocation.Panel, supportsFileReferences: true, supportsAdditionalParticipants: this.chatOptions.location === ChatAgentLocation.Panel, rendererOptions: { - renderCodeBlockPills: this.chatOptions.location === ChatAgentLocation.EditingSession, + renderCodeBlockPills: mode => mode !== ChatMode.Chat, renderTextEditsAsSummary: (uri) => { - return this.chatOptions.location === ChatAgentLocation.EditingSession; + return this.chatService.isEditingLocation(this.chatOptions.location); }, - referencesExpandedWhenEmptyResponse: this.chatOptions.location !== ChatAgentLocation.EditingSession, - progressMessageAtBottomOfResponse: this.chatOptions.location === ChatAgentLocation.EditingSession, + referencesExpandedWhenEmptyResponse: !this.chatService.isEditingLocation(this.chatOptions.location), + progressMessageAtBottomOfResponse: this.chatService.isEditingLocation(this.chatOptions.location), }, editorOverflowWidgetsDomNode: editorOverflowNode, - enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel || this.chatOptions.location === ChatAgentLocation.EditingSession, - enableWorkingSet: this.chatOptions.location === ChatAgentLocation.EditingSession ? 'explicit' : undefined + enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel || this.chatService.isEditingLocation(this.chatOptions.location), + enableWorkingSet: this.chatService.isEditingLocation(this.chatOptions.location) ? 'explicit' : undefined, + supportsChangingModes: this.chatService.isEditingLocation(this.chatOptions.location), }, { listForeground: SIDE_BAR_FOREGROUND, @@ -201,7 +205,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this._register(this._widget.onDidClear(() => this.clear())); this._widget.render(parent); - const sessionId = this.getSessionId(); + const info = this.getTransferredOrPersistedSessionInfo(); const disposeListener = this._register(this.chatService.onDidDisposeSession((e) => { // Render the welcome view if provider registration fails, eg when signed out. This activates for any session, but the problem is the same regardless if (e.reason === 'initializationFailed') { @@ -210,9 +214,9 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this._onDidChangeViewWelcomeState.fire(); } })); - const model = sessionId ? this.chatService.getOrRestoreSession(sessionId) : undefined; + const model = info.sessionId ? this.chatService.getOrRestoreSession(info.sessionId) : undefined; - this.updateModel(model); + this.updateModel(model, info.inputValue || info.mode ? { inputState: { chatMode: info.mode }, inputValue: info.inputValue } : undefined); } catch (e) { this.logService.error(e); throw e; diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 12fba91fa44..2390d3247ed 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -8,11 +8,12 @@ import { Button } from '../../../../base/browser/ui/button/button.js'; import { ITreeContextMenuEvent, ITreeElement } from '../../../../base/browser/ui/tree/tree.js'; import { disposableTimeout, timeout } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; +import { memoize } from '../../../../base/common/decorators.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable, combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { combinedDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; import { autorunWithStore, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; @@ -32,23 +33,24 @@ import { ServiceCollection } from '../../../../platform/instantiation/common/ser import { WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { IProductService } from '../../../../platform/product/common/productService.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService, IChatWelcomeMessageContent, isChatWelcomeMessageContent } from '../common/chatAgents.js'; +import { checkModeOption } from '../common/chat.js'; +import { IChatAgentCommand, IChatAgentData, IChatAgentService, IChatWelcomeMessageContent, isChatWelcomeMessageContent } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; +import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, WorkingSetEntryState } from '../common/chatEditingService.js'; import { ChatPauseState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js'; -import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, formatChatQuestion } from '../common/chatParserTypes.js'; +import { chatAgentLeader, ChatRequestAgentPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../common/chatParserTypes.js'; import { ChatRequestParser } from '../common/chatRequestParser.js'; import { IChatFollowup, IChatLocationData, IChatSendRequestOptions, IChatService } from '../common/chatService.js'; import { IChatSlashCommandService } from '../common/chatSlashCommands.js'; import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../common/chatViewModel.js'; import { IChatInputState } from '../common/chatWidgetHistoryService.js'; import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js'; +import { ChatAgentLocation, ChatConfiguration, ChatMode } from '../common/constants.js'; import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from './chat.js'; import { ChatAccessibilityProvider } from './chatAccessibilityProvider.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; @@ -150,6 +152,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private container!: HTMLElement; private welcomeMessageContainer!: HTMLElement; private persistedWelcomeMessage: IChatWelcomeMessageContent | undefined; + private readonly welcomePart: MutableDisposable = this._register(new MutableDisposable()); private bodyDimension: dom.Dimension | undefined; private visibleChangeCount = 0; @@ -158,6 +161,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private canRequestBePaused: IContextKey; private agentInInput: IContextKey; + private _visible = false; public get visible() { return this._visible; @@ -201,7 +205,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return { text: '', parts: [] }; } - this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent }); + this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentMode }); } return this.parsedChatRequest; @@ -218,6 +222,11 @@ export class ChatWidget extends Disposable implements IChatWidget { readonly viewContext: IChatWidgetViewContext; + @memoize + get isUnifiedPanelWidget(): boolean { + return this._location.location === ChatAgentLocation.Panel && !!this.viewOptions.supportsChangingModes && this.configurationService.getValue(ChatConfiguration.UnifiedChatView); + } + constructor( location: ChatAgentLocation | IChatWidgetLocationOptions, _viewContext: IChatWidgetViewContext | undefined, @@ -238,7 +247,6 @@ export class ChatWidget extends Disposable implements IChatWidget { @IChatEditingService chatEditingService: IChatEditingService, @IStorageService private readonly storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IProductService private readonly productService: IProductService, ) { super(); @@ -255,6 +263,8 @@ export class ChatWidget extends Disposable implements IChatWidget { ChatContextKeys.inChatSession.bindTo(contextKeyService).set(true); ChatContextKeys.location.bindTo(contextKeyService).set(this._location.location); ChatContextKeys.inQuickChat.bindTo(contextKeyService).set(isQuickChat(this)); + ChatContextKeys.inUnifiedChat.bindTo(contextKeyService) + .set(this._location.location === ChatAgentLocation.Panel && !!this.viewOptions.supportsChangingModes && this.configurationService.getValue(ChatConfiguration.UnifiedChatView)); this.agentInInput = ChatContextKeys.inputHasAgent.bindTo(contextKeyService); this.requestInProgress = ChatContextKeys.requestInProgress.bindTo(contextKeyService); this.isRequestPaused = ChatContextKeys.isRequestPaused.bindTo(contextKeyService); @@ -345,7 +355,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.renderChatEditingSessionState(); })); - if (this._location.location === ChatAgentLocation.EditingSession) { + if (this._location.location === ChatAgentLocation.EditingSession || this.chatService.unifiedViewEnabled) { let currentEditSession: IChatEditingSession | undefined = undefined; this._register(this.onDidChangeViewModel(async () => { const sessionId = this._viewModel?.sessionId; @@ -471,7 +481,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container = dom.append(parent, $('.interactive-session')); this.welcomeMessageContainer = dom.append(this.container, $('.chat-welcome-view-container', { style: 'display: none' })); - this.renderWelcomeViewContentIfNeeded(); if (renderInputOnTop) { this.createInput(this.container, { renderFollowups, renderStyle }); this.listContainer = dom.append(this.container, $(`.interactive-list`)); @@ -480,6 +489,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.createInput(this.container, { renderFollowups, renderStyle }); } + this.renderWelcomeViewContentIfNeeded(); this.createList(this.listContainer, { ...this.viewOptions.rendererOptions, renderStyle }); const scrollDownButton = this._register(new Button(this.listContainer, { @@ -540,7 +550,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (!this.viewModel) { return; } - this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent }); + this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentMode }); this._onDidChangeParsedInput.fire(); } @@ -616,7 +626,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (this.lastItem && isResponseVM(this.lastItem) && this.lastItem.isComplete) { this.renderFollowups(this.lastItem.replyFollowups, this.lastItem); } else if (!treeItems.length && this.viewModel) { - this.renderFollowups(this.viewModel.model.sampleQuestions); + this.renderSampleQuestions(); } else { this.renderFollowups(undefined); } @@ -629,21 +639,22 @@ export class ChatWidget extends Disposable implements IChatWidget { } const numItems = this.viewModel?.getItems().length ?? 0; - const welcomeContent = this.viewModel?.model.welcomeMessage ?? this.persistedWelcomeMessage; - if (welcomeContent && !numItems && (this.welcomeMessageContainer.children.length === 0 || this.location === ChatAgentLocation.EditingSession)) { + const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentMode); + const welcomeContent = defaultAgent?.metadata.welcomeMessageContent ?? this.persistedWelcomeMessage; + if (welcomeContent && !numItems && (this.welcomeMessageContainer.children.length === 0 || this.chatService.unifiedViewEnabled)) { dom.clearNode(this.welcomeMessageContainer); const tips = this.viewOptions.supportsAdditionalParticipants ? new MarkdownString(localize('chatWidget.tips', "{0} or type {1} to attach context\n\n{2} to chat with extensions\n\nType {3} to use commands", '$(attach)', '#', '$(mention)', '/'), { supportThemeIcons: true }) : new MarkdownString(localize('chatWidget.tips.withoutParticipants', "{0} or type {1} to attach context", '$(attach)', '#'), { supportThemeIcons: true }); - const welcomePart = this._register(this.instantiationService.createInstance( + this.welcomePart.value = this.instantiationService.createInstance( ChatViewWelcomePart, { ...welcomeContent, tips, }, { location: this.location, - isWidgetWelcomeViewContent: true + isWidgetAgentWelcomeViewContent: this.input?.currentMode === ChatMode.Agent } - )); - dom.append(this.welcomeMessageContainer, welcomePart.element); + ); + dom.append(this.welcomeMessageContainer, this.welcomePart.value.element); } if (this.viewModel) { @@ -656,13 +667,20 @@ export class ChatWidget extends Disposable implements IChatWidget { if (!this.inputPart) { return; } - this.inputPart.renderChatEditingSessionState(this._editingSession.get() ?? null, this); + this.inputPart.renderChatEditingSessionState(this._editingSession.get() ?? null); if (this.bodyDimension) { this.layout(this.bodyDimension.height, this.bodyDimension.width); } } + private renderSampleQuestions() { + if (this.viewModel) { + // TODO@roblourens hack- only Chat mode supports sample questions + this.renderFollowups(this.input.currentMode === ChatMode.Chat ? this.viewModel.model.sampleQuestions : undefined); + } + } + private async renderFollowups(items: IChatFollowup[] | undefined, response?: IChatResponseViewModel): Promise { this.inputPart.renderFollowups(items, response); @@ -697,7 +715,8 @@ export class ChatWidget extends Disposable implements IChatWidget { const rendererDelegate: IChatRendererDelegate = { getListLength: () => this.tree.getNode(null).visibleChildrenCount, onDidScroll: this.onDidScroll, - container: listContainer + container: listContainer, + currentChatMode: () => this.input.currentMode, }; // Create a dom element to hold UI from editor widgets embedded in chat messages @@ -838,7 +857,8 @@ export class ChatWidget extends Disposable implements IChatWidget { menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }, editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, enableImplicitContext: this.viewOptions.enableImplicitContext, - renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit' + renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit', + supportsChangingModes: this.viewOptions.supportsChangingModes, }, this.styles, () => this.collectInputState() @@ -861,7 +881,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } let msg = ''; - if (e.followup.agentId && e.followup.agentId !== this.chatAgentService.getDefaultAgent(this.location)?.id) { + if (e.followup.agentId && e.followup.agentId !== this.chatAgentService.getDefaultAgent(this.location, this.input.currentMode)?.id) { const agent = this.chatAgentService.getAgent(e.followup.agentId); if (!agent) { return; @@ -919,6 +939,11 @@ export class ChatWidget extends Disposable implements IChatWidget { // Tools agent loads -> welcome content changes this.renderWelcomeViewContentIfNeeded(); })); + this._register(this.input.onDidChangeCurrentChatMode(() => { + this.renderSampleQuestions(); + this.renderWelcomeViewContentIfNeeded(); + this.refreshParsedInput(); + })); } private onDidStyleChange(): void { @@ -952,10 +977,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.requestInProgress.set(this.viewModel.requestInProgress); this.isRequestPaused.set(this.viewModel.requestPausibility === ChatPauseState.Paused); - // todo@connor4312: disabled for stable 1.97 release. - if (this.productService.quality !== 'stable') { - this.canRequestBePaused.set(this.viewModel.requestPausibility !== ChatPauseState.NotPausable); - } + this.canRequestBePaused.set(this.viewModel.requestPausibility !== ChatPauseState.NotPausable); this.onDidChangeItems(); if (events.some(e => e?.kind === 'addRequest') && this.visible) { @@ -1083,7 +1105,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (this.viewModel) { this._onDidAcceptInput.fire(); - if (!this.viewOptions.autoScroll) { + if (!checkModeOption(this.input.currentMode, this.viewOptions.autoScroll)) { this.scrollLock = false; } @@ -1108,35 +1130,8 @@ export class ChatWidget extends Disposable implements IChatWidget { let attachedContext = this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); if (this.viewOptions.enableWorkingSet !== undefined) { - const currentEditingSession = this._editingSession; - - const unconfirmedSuggestions = new ResourceSet(); const uniqueWorkingSetEntries = new ResourceSet(); // NOTE: this is used for bookkeeping so the UI can avoid rendering references in the UI that are already shown in the working set const editingSessionAttachedContext: IChatRequestVariableEntry[] = attachedContext; - // Pick up everything that the user sees is part of the working set. - for (const [uri, state] of currentEditingSession.get()?.workingSet || []) { - // Skip over any suggested files that haven't been confirmed yet in the working set - if (state.state === WorkingSetEntryState.Suggested) { - unconfirmedSuggestions.add(uri); - } else { - uniqueWorkingSetEntries.add(uri); - editingSessionAttachedContext.unshift(this.attachmentModel.asVariableEntry(uri, undefined, state.isMarkedReadonly)); - } - } - - // add prompt instruction references to the attached context, if enabled - const promptInstructionUris = new ResourceSet(promptInstructions.chatAttachments.map((v) => v.value) as URI[]); - if (instructionsEnabled) { - editingSessionAttachedContext - .push(...promptInstructions.chatAttachments); - } - - for (const file of uniqueWorkingSetEntries) { - // Make sure that any files that we sent are part of the working set - // but do not permanently add file variables from previous requests to the working set - // since the user may subsequently edit the chat history - currentEditingSession.get()?.addFileToWorkingSet(file); - } // Collect file variables from previous requests before sending the request const previousRequests = this.viewModel.model.getRequests(); @@ -1144,7 +1139,7 @@ export class ChatWidget extends Disposable implements IChatWidget { for (const variable of request.variableData.variables) { if (URI.isUri(variable.value) && variable.isFile) { const uri = variable.value; - if (!uniqueWorkingSetEntries.has(uri) && !promptInstructionUris.has(uri)) { + if (!uniqueWorkingSetEntries.has(uri)) { editingSessionAttachedContext.push(variable); uniqueWorkingSetEntries.add(variable.value); } @@ -1164,19 +1159,20 @@ export class ChatWidget extends Disposable implements IChatWidget { actualSize: number; }; this.telemetryService.publicLog2('chatEditing/workingSetSize', { originalSize: uniqueWorkingSetEntries.size, actualSize: uniqueWorkingSetEntries.size }); - currentEditingSession.get()?.remove(WorkingSetEntryRemovalReason.User, ...unconfirmedSuggestions); } this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionId); const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { + mode: this.inputPart.currentMode, userSelectedModelId: this.inputPart.currentLanguageModel, location: this.location, locationData: this._location.resolveData?.(), - parserContext: { selectedAgent: this._lastSelectedAgent }, + parserContext: { selectedAgent: this._lastSelectedAgent, mode: this.inputPart.currentMode }, attachedContext, noCommandDetection: options?.noCommandDetection, hasInstructionAttachments: this.inputPart.hasInstructionAttachments, + userSelectedTools: this.input.currentMode === ChatMode.Agent ? ChatInputPart.selectedToolsModel.tools.get().map(tool => tool.id) : undefined }); if (result) { @@ -1242,8 +1238,10 @@ export class ChatWidget extends Disposable implements IChatWidget { const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight - 2; const listHeight = Math.max(0, height - inputPartHeight); - if (!this.viewOptions.autoScroll) { + if (!checkModeOption(this.input.currentMode, this.viewOptions.autoScroll)) { this.listContainer.style.setProperty('--chat-current-response-min-height', listHeight * .75 + 'px'); + } else { + this.listContainer.style.removeProperty('--chat-current-response-min-height'); } this.tree.layout(listHeight, width); @@ -1262,7 +1260,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const lastItem = this.viewModel?.getItems().at(-1); const lastResponseIsRendering = isResponseVM(lastItem) && lastItem.renderData; - if (lastElementVisible && (!lastResponseIsRendering || this.viewOptions.autoScroll)) { + if (lastElementVisible && (!lastResponseIsRendering || checkModeOption(this.input.currentMode, this.viewOptions.autoScroll))) { this.scrollToEnd(); } @@ -1373,8 +1371,9 @@ export class ChatWidget extends Disposable implements IChatWidget { saveState(): void { this.inputPart.saveState(); - if (this.viewModel?.model.welcomeMessage) { - this.storageService.store(`${PersistWelcomeMessageContentKey}.${this.location}`, this.viewModel?.model.welcomeMessage, StorageScope.APPLICATION, StorageTarget.MACHINE); + const welcomeContent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentMode)?.metadata.welcomeMessageContent; + if (welcomeContent) { + this.storageService.store(`${PersistWelcomeMessageContentKey}.${this.location}`, welcomeContent, StorageScope.APPLICATION, StorageTarget.MACHINE); } } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 0bf919d40f0..67848145eb8 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce } from '../../../../../base/common/arrays.js'; +import { coalesce, groupBy } from '../../../../../base/common/arrays.js'; +import { assertNever } from '../../../../../base/common/assert.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { isCancellationError } from '../../../../../base/common/errors.js'; @@ -35,7 +36,7 @@ import { IWorkspaceContextService } from '../../../../../platform/workspace/comm import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType } from '../../../../services/search/common/search.js'; import { ISymbolQuickPickItem } from '../../../search/browser/symbolsQuickAccess.js'; import { IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js'; -import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js'; +import { IChatRequestProblemsVariable, IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js'; import { IChatWidget } from '../chat.js'; import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js'; import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js'; @@ -348,6 +349,7 @@ export class SelectAndInsertFolderAction extends Action2 { context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ id: 'vscode.folder', isFile: false, + isDirectory: true, prefix: 'folder', range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length }, data: folder @@ -392,6 +394,7 @@ export async function createFolderQuickPick(accessor: ServicesAccessor): Promise searchFolders( workspace, value, + true, undefined, undefined, configurationService, @@ -419,6 +422,7 @@ export async function createFolderQuickPick(accessor: ServicesAccessor): Promise type: 'item', id: folder.toString(), resource: folder, + alwaysShow: true, label: basename(folder), description: labelService.getUriLabel(dirname(folder), { relative: true }), iconClass: ThemeIcon.asClassName(Codicon.folder), @@ -449,11 +453,14 @@ export async function getTopLevelFolders(workspaces: URI[], fileService: IFileSe export async function searchFolders( workspace: URI, pattern: string, + fuzzyMatch: boolean, token: CancellationToken | undefined, cacheKey: string | undefined, configurationService: IConfigurationService, searchService: ISearchService ): Promise { + const segmentMatchPattern = caseInsensitiveGlobPattern(fuzzyMatch ? fuzzyMatchingGlobPattern(pattern) : continousMatchingGlobPattern(pattern)); + const searchExcludePattern = getExcludes(configurationService.getValue({ resource: workspace })) || {}; const searchOptions: IFileQuery = { folderQueries: [{ @@ -468,7 +475,7 @@ export async function searchFolders( let folderResults: ISearchComplete | undefined; try { - folderResults = await searchService.fileSearch({ ...searchOptions, filePattern: `**/*${pattern}*/**` }, token); + folderResults = await searchService.fileSearch({ ...searchOptions, filePattern: `**/${segmentMatchPattern}/**` }, token); } catch (e) { if (!isCancellationError(e)) { throw e; @@ -479,13 +486,40 @@ export async function searchFolders( return []; } - const folderResources = getMatchingFoldersFromFiles(folderResults.results.map(result => result.resource), workspace, pattern); + const folderResources = getMatchingFoldersFromFiles(folderResults.results.map(result => result.resource), workspace, segmentMatchPattern); return folderResources; } +function fuzzyMatchingGlobPattern(pattern: string): string { + if (!pattern) { + return '*'; + } + return '*' + pattern.split('').join('*') + '*'; +} + +function continousMatchingGlobPattern(pattern: string): string { + if (!pattern) { + return '*'; + } + return '*' + pattern + '*'; +} + +function caseInsensitiveGlobPattern(pattern: string): string { + let caseInsensitiveFilePattern = ''; + for (let i = 0; i < pattern.length; i++) { + const char = pattern[i]; + if (/[a-zA-Z]/.test(char)) { + caseInsensitiveFilePattern += `[${char.toLowerCase()}${char.toUpperCase()}]`; + } else { + caseInsensitiveFilePattern += char; + } + } + return caseInsensitiveFilePattern; +} + // TODO: remove this and have support from the search service -function getMatchingFoldersFromFiles(resources: URI[], workspace: URI, pattern: string): URI[] { +function getMatchingFoldersFromFiles(resources: URI[], workspace: URI, segmentMatchPattern: string): URI[] { const uniqueFolders = new ResourceSet(); for (const resource of resources) { const relativePathToRoot = relativePath(workspace, resource); @@ -505,7 +539,7 @@ function getMatchingFoldersFromFiles(resources: URI[], workspace: URI, pattern: for (const folderResource of uniqueFolders) { const stats = folderResource.path.split('/'); const dirStat = stats[stats.length - 1]; - if (!dirStat || !glob.match(`*${pattern}*`, dirStat)) { + if (!dirStat || !glob.match(segmentMatchPattern, dirStat)) { continue; } @@ -649,56 +683,132 @@ export class AddDynamicVariableAction extends Action2 { } registerAction2(AddDynamicVariableAction); -export async function createMarkersQuickPick(accessor: ServicesAccessor): Promise { - const markers = accessor.get(IMarkerService).read(); +export async function createMarkersQuickPick(accessor: ServicesAccessor, level: 'problem' | 'file', onBackgroundAccept?: (item: IDiagnosticVariableEntryFilterData[]) => void): Promise { + const markers = accessor.get(IMarkerService).read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }); if (!markers.length) { return; } const uriIdentityService = accessor.get(IUriIdentityService); const labelService = accessor.get(ILabelService); - markers.sort((a, b) => uriIdentityService.extUri.compare(a.resource, b.resource) || b.severity - a.severity); + const grouped = groupBy(markers, (a, b) => uriIdentityService.extUri.compare(a.resource, b.resource)); const severities = new Set(); type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData }; const items: (MarkerPickItem | IQuickPickSeparator)[] = []; - for (const marker of markers) { - if (!uriIdentityService.extUri.isEqual(marker.resource, (items.at(-1) as MarkerPickItem)?.resource)) { - items.push({ type: 'separator', label: labelService.getUriLabel(marker.resource, { relative: true }) }); - } - severities.add(marker.severity); - items.push({ - type: 'item', - resource: marker.resource, - label: marker.message, - description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn), - entry: { filterUri: marker.resource, filterRange: { startLineNumber: marker.startLineNumber, endLineNumber: marker.endLineNumber, startColumn: marker.startColumn, endColumn: marker.endColumn } } - }); - } - - if (items.length === 2) { // single error in a URI - return (items[1] as MarkerPickItem).entry; - } - - if (items.length > 2) { - if (severities.has(MarkerSeverity.Error)) { - items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Errors'), entry: { filterSeverity: MarkerSeverity.Error } }); - } - if (severities.has(MarkerSeverity.Warning)) { - items.unshift({ type: 'item', label: localize('markers.panel.allWarnings', 'All Warnings'), entry: { filterSeverity: MarkerSeverity.Warning } }); - } - if (severities.has(MarkerSeverity.Info)) { - items.unshift({ type: 'item', label: localize('markers.panel.allInfos', 'All Infos'), entry: { filterSeverity: MarkerSeverity.Info } }); + let pickCount = 0; + for (const group of grouped) { + const resource = group[0].resource; + if (level === 'problem') { + items.push({ type: 'separator', label: labelService.getUriLabel(resource, { relative: true }) }); + for (const marker of group) { + pickCount++; + severities.add(marker.severity); + items.push({ + type: 'item', + resource: marker.resource, + label: marker.message, + description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn), + entry: IDiagnosticVariableEntryFilterData.fromMarker(marker), + }); + } + } else if (level === 'file') { + const entry = { filterUri: resource }; + pickCount++; + items.push({ + type: 'item', + resource, + label: IDiagnosticVariableEntryFilterData.label(entry), + description: group[0].message + (group.length > 1 ? localize('problemsMore', '+ {0} more', group.length - 1) : ''), + entry, + }); + for (const marker of group) { + severities.add(marker.severity); + } + } else { + assertNever(level); } } + if (pickCount < 2) { // single error in a URI + return items.find((i): i is MarkerPickItem => i.type === 'item')?.entry; + } + + if (level === 'file') { + items.unshift({ type: 'separator', label: localize('markers.panel.files', 'Files') }); + } + + items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Problems'), entry: { filterSeverity: MarkerSeverity.Info } }); const quickInputService = accessor.get(IQuickInputService); - const quickPick = quickInputService.createQuickPick({ useSeparators: true }); + const store = new DisposableStore(); + const quickPick = store.add(quickInputService.createQuickPick({ useSeparators: true })); + quickPick.canAcceptInBackground = !onBackgroundAccept; quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...'); quickPick.items = items; - return quickInputService.pick(items, { canPickMany: false }).then(v => v?.entry); + return new Promise(resolve => { + store.add(quickPick.onDidHide(() => resolve(undefined))); + store.add(quickPick.onDidAccept(ev => { + if (ev.inBackground) { + onBackgroundAccept?.(quickPick.selectedItems.map(i => i.entry)); + } else { + resolve(quickPick.selectedItems[0]?.entry); + quickPick.dispose(); + } + })); + quickPick.show(); + }).finally(() => store.dispose()); } +export class SelectAndInsertProblemAction extends Action2 { + static readonly Name = 'problems'; + static readonly ID = 'workbench.action.chat.selectAndInsertProblems'; + + constructor() { + super({ + id: SelectAndInsertProblemAction.ID, + title: '' // not displayed + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const logService = accessor.get(ILogService); + const context = args[0]; + if (!isSelectAndInsertActionContext(context)) { + return; + } + + const doCleanup = () => { + // Failed, remove the dangling `problem` + context.widget.inputEditor.executeEdits('chatInsertProblems', [{ range: context.range, text: `` }]); + }; + + const pick = await createMarkersQuickPick(accessor, 'file'); + if (!pick) { + doCleanup(); + return; + } + + const editor = context.widget.inputEditor; + const originalRange = context.range; + const insertText = `#${SelectAndInsertProblemAction.Name}:${pick.filterUri ? basename(pick.filterUri) : MarkerSeverity.toString(pick.filterSeverity!)}`; + + const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startColumn + insertText.length); + const success = editor.executeEdits('chatInsertProblems', [{ range: varRange, text: insertText + ' ' }]); + if (!success) { + logService.trace(`SelectAndInsertProblemsAction: failed to insert "${insertText}"`); + doCleanup(); + return; + } + + context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ + id: 'vscode.problems', + prefix: SelectAndInsertProblemAction.Name, + range: varRange, + data: { id: 'vscode.problems', filter: pick } satisfies IChatRequestProblemsVariable, + }); + } +} +registerAction2(SelectAndInsertProblemAction); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index f4673ffc858..afcd62f2218 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -6,6 +6,7 @@ import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; import { autorun } from '../../../../../base/common/observable.js'; import { basename } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -16,10 +17,11 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { EditorsOrder } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { ChatAgentLocation } from '../../common/chatAgents.js'; +import { getNotebookEditorFromEditorPane, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; import { IBaseChatRequestVariableEntry, IChatRequestImplicitVariableEntry } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; +import { ChatAgentLocation } from '../../common/constants.js'; import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; @@ -44,7 +46,7 @@ export class ChatImplicitContextContribution extends Disposable implements IWork const activeEditorDisposables = this._register(new DisposableStore()); this._register(Event.runAndSubscribe( - editorService.onDidVisibleEditorsChange, + editorService.onDidActiveEditorChange, (() => { activeEditorDisposables.clear(); const codeEditor = this.findActiveCodeEditor(); @@ -58,6 +60,17 @@ export class ChatImplicitContextContribution extends Disposable implements IWork 500)(() => this.updateImplicitContext())); } + const notebookEditor = this.findActiveNotebookEditor(); + if (notebookEditor) { + activeEditorDisposables.add(Event.debounce( + Event.any( + notebookEditor.onDidChangeModel, + notebookEditor.onDidChangeActiveCell + ), + () => undefined, + 500)(() => this.updateImplicitContext())); + } + this.updateImplicitContext(); }))); this._register(autorun((reader) => { @@ -79,12 +92,19 @@ export class ChatImplicitContextContribution extends Disposable implements IWork widget.input.implicitContext.setValue(undefined, false); } })); + this._register(this.chatWidgetService.onDidAddWidget(async (widget) => { + await this.updateImplicitContext(widget); + })); } private findActiveCodeEditor(): ICodeEditor | undefined { const codeEditor = this.codeEditorService.getActiveCodeEditor(); if (codeEditor) { const model = codeEditor.getModel(); + if (model?.uri.scheme === Schemas.vscodeNotebookCell) { + return undefined; + } + if (model) { return codeEditor; } @@ -107,6 +127,10 @@ export class ChatImplicitContextContribution extends Disposable implements IWork return undefined; } + private findActiveNotebookEditor(): INotebookEditor | undefined { + return getNotebookEditorFromEditorPane(this.editorService.activeEditorPane); + } + private async updateImplicitContext(updateWidget?: IChatWidget): Promise { const cancelTokenSource = this._currentCancelTokenSource.value = new CancellationTokenSource(); const codeEditor = this.findActiveCodeEditor(); @@ -134,6 +158,16 @@ export class ChatImplicitContextContribution extends Disposable implements IWork } } + const notebookEditor = this.findActiveNotebookEditor(); + if (notebookEditor) { + const activeCell = notebookEditor.getActiveCell(); + if (activeCell) { + newValue = activeCell.uri; + } else { + newValue = notebookEditor.textModel?.uri; + } + } + const uri = newValue instanceof URI ? newValue : newValue?.uri; if (uri && await this.ignoredFilesService.fileIsIgnored(uri, cancelTokenSource.token)) { newValue = undefined; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 0f4cd25c10d..92783d78ea0 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -27,6 +27,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { IMarkerService } from '../../../../../platform/markers/common/markers.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js'; @@ -40,11 +41,12 @@ import { IChatEditingService } from '../../common/chatEditingService.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPart, ChatRequestToolPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js'; import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; import { IDynamicVariable } from '../../common/chatVariables.js'; +import { ChatMode } from '../../common/constants.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; import { ChatEditingSessionSubmitAction, ChatSubmitAction } from '../actions/chatExecuteActions.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { ChatInputPart } from '../chatInputPart.js'; -import { ChatDynamicVariableModel, getTopLevelFolders, searchFolders, SelectAndInsertFolderAction, SelectAndInsertFileAction, SelectAndInsertSymAction } from './chatDynamicVariables.js'; +import { ChatDynamicVariableModel, SelectAndInsertFileAction, SelectAndInsertFolderAction, SelectAndInsertProblemAction, SelectAndInsertSymAction, getTopLevelFolders, searchFolders } from './chatDynamicVariables.js'; class SlashCommandCompletions extends Disposable { constructor( @@ -214,7 +216,7 @@ class AgentCompletions extends Disposable { provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); const viewModel = widget?.viewModel; - if (!widget || !viewModel) { + if (!widget || !viewModel || widget.input.currentMode !== ChatMode.Chat) { return; } @@ -264,7 +266,7 @@ class AgentCompletions extends Disposable { return { suggestions: justAgents.concat( coalesce(agents.flatMap(agent => agent.slashCommands.map((c, i) => { - if (agent.isDefault && this.chatAgentService.getDefaultAgent(widget.location)?.id !== agent.id) { + if (agent.isDefault && this.chatAgentService.getDefaultAgent(widget.location, widget.input.currentMode)?.id !== agent.id) { return; } @@ -304,7 +306,7 @@ class AgentCompletions extends Disposable { provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); const viewModel = widget?.viewModel; - if (!widget || !viewModel) { + if (!widget || !viewModel || widget.input.currentMode !== ChatMode.Chat) { return; } @@ -323,7 +325,7 @@ class AgentCompletions extends Disposable { return { suggestions: coalesce(agents.flatMap(agent => agent.slashCommands.map((c, i) => { - if (agent.isDefault && this.chatAgentService.getDefaultAgent(widget.location)?.id !== agent.id) { + if (agent.isDefault && this.chatAgentService.getDefaultAgent(widget.location, widget.input.currentMode)?.id !== agent.id) { return; } @@ -365,7 +367,7 @@ class AgentCompletions extends Disposable { } const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (widget?.location !== ChatAgentLocation.Panel) { + if (widget?.location !== ChatAgentLocation.Panel || widget.input.currentMode !== ChatMode.Chat) { return; } @@ -467,6 +469,7 @@ class BuiltinDynamicCompletions extends Disposable { @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IFileService private readonly fileService: IFileService, + @IMarkerService markerService: IMarkerService, ) { super(); @@ -560,7 +563,7 @@ class BuiltinDynamicCompletions extends Disposable { sortText: 'z', command: { id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { - id: 'vscode.file', + id: 'vscode.selection', prefix: 'file', isFile: true, range: { startLineNumber: range.replace.startLineNumber, startColumn: range.replace.startColumn, endLineNumber: range.replace.endLineNumber, endColumn: range.replace.startColumn + text.length }, @@ -598,6 +601,30 @@ class BuiltinDynamicCompletions extends Disposable { return result; }); + // Problems completions, we just attach all problems in this case + this.registerVariableCompletions(SelectAndInsertProblemAction.Name, ({ widget, range, position, model }, token) => { + const stats = markerService.getStatistics(); + if (!stats.errors && !stats.warnings) { + return null; + } + + const result: CompletionList = { suggestions: [] }; + + const completedText = `${chatVariableLeader}${SelectAndInsertProblemAction.Name}:`; + const afterTextRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + completedText.length); + result.suggestions.push({ + label: `${chatVariableLeader}${SelectAndInsertProblemAction.Name}`, + insertText: completedText, + documentation: localize('pickProblemsLabel', "Problems in your workspace"), + range, + kind: CompletionItemKind.Text, + command: { id: SelectAndInsertProblemAction.ID, title: SelectAndInsertProblemAction.ID, arguments: [{ widget, range: afterTextRange }] }, + sortText: 'z' + }); + + return result; + }); + this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg))); this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); @@ -665,7 +692,7 @@ class BuiltinDynamicCompletions extends Disposable { const len = result.suggestions.length; // RELATED FILES - if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel && this._chatEditingService.getEditingSession(widget.viewModel.sessionId)) { + if (widget.input.currentMode !== ChatMode.Chat && widget.viewModel && this._chatEditingService.getEditingSession(widget.viewModel.sessionId)) { const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), widget.attachmentModel.fileAttachments, token), 200)) ?? []; for (const relatedFileGroup of relatedFiles) { for (const relatedFile of relatedFileGroup.files) { @@ -755,6 +782,7 @@ class BuiltinDynamicCompletions extends Disposable { id: 'vscode.folder', prefix: 'folder', isFile: false, + isDirectory: true, range: { startLineNumber: info.replace.startLineNumber, startColumn: info.replace.startColumn, endLineNumber: info.replace.endLineNumber, endColumn: info.replace.startColumn + text.length }, data: resource })] @@ -781,7 +809,7 @@ class BuiltinDynamicCompletions extends Disposable { const cacheKey = this.updateCacheKey(); - const folders = await Promise.all(workspaces.map(workspace => searchFolders(workspace, pattern, token, cacheKey.key, this.configurationService, this.searchService))); + const folders = await Promise.all(workspaces.map(workspace => searchFolders(workspace, pattern, true, token, cacheKey.key, this.configurationService, this.searchService))); for (const resource of folders.flat()) { if (seen.has(resource)) { // already included via history diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index e0997913f21..06957dd285a 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -112,7 +112,7 @@ class InputEditorDecorations extends Disposable { } if (!inputValue) { - const defaultAgent = this.chatAgentService.getDefaultAgent(this.widget.location); + const defaultAgent = this.chatAgentService.getDefaultAgent(this.widget.location, this.widget.input.currentMode); const decoration: IDecorationOptions[] = [ { range: { @@ -301,7 +301,7 @@ class ChatTokenDeleter extends Disposable { // If this was a simple delete, try to find out whether it was inside a token if (!change.text && this.widget.viewModel) { - const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, widget.location, { selectedAgent: previousSelectedAgent }); + const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, widget.location, { selectedAgent: previousSelectedAgent, mode: this.widget.input.currentMode }); // For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestToolPart); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts index 3860b5d5efa..44c4d6d8946 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts @@ -4,14 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Event } from '../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; import { autorun } from '../../../../../base/common/observable.js'; +import { isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import { IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { IChatEditingService, IChatEditingSession } from '../../common/chatEditingService.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; export class ChatRelatedFilesContribution extends Disposable implements IWorkbenchContribution { @@ -22,7 +23,7 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben constructor( @IChatEditingService private readonly chatEditingService: IChatEditingService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, ) { super(); @@ -50,7 +51,7 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) .then((files) => { - if (!files?.length || !widget.viewModel?.sessionId) { + if (!files?.length || !widget.viewModel?.sessionId || !widget.input.relatedFiles) { return; } @@ -59,13 +60,13 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben return; // Might have disposed while we were calculating } - const existingFiles = new ResourceSet(widget.attachmentModel.fileAttachments); + const existingFiles = new ResourceSet([...widget.attachmentModel.fileAttachments, ...widget.input.relatedFiles.removedFiles]); if (!existingFiles.size) { return; } // Pick up to 2 related files - const newSuggestions = new ResourceMap<{ description: string; group: string }>(); + const newSuggestions = new ResourceMap(); for (const group of files) { for (const file of group.files) { if (newSuggestions.size >= 2) { @@ -74,24 +75,12 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben if (existingFiles.has(file.uri)) { continue; } - newSuggestions.set(file.uri, { group: group.group, description: file.description }); + newSuggestions.set(file.uri, localize('relatedFile', "{0} (Suggested)", file.description)); existingFiles.add(file.uri); } } - // Remove the existing related file suggestions from the working set - const existingSuggestedEntriesToRemove: URI[] = []; - for (const entry of currentEditingSession.workingSet) { - if (entry[1].state === WorkingSetEntryState.Suggested && !newSuggestions.has(entry[0])) { - existingSuggestedEntriesToRemove.push(entry[0]); - } - } - currentEditingSession?.remove(WorkingSetEntryRemovalReason.Programmatic, ...existingSuggestedEntriesToRemove); - - // Add the new related file suggestions to the working set - for (const [uri, data] of newSuggestions) { - currentEditingSession.addFileToWorkingSet(uri, localize('relatedFile', "{0} (Suggested)", data.description), WorkingSetEntryState.Suggested); - } + widget.input.relatedFiles.value = [...newSuggestions.entries()].map(([uri, description]) => ({ uri, description })); }) .finally(() => { this._currentRelatedFilesRetrievalOperation = undefined; @@ -115,6 +104,10 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben disposableStore.add(currentEditingSession.onDidDispose(() => { disposableStore.dispose(); })); + disposableStore.add(widget.onDidAcceptInput(() => { + widget.input.relatedFiles?.clear(); + this._updateRelatedFileSuggestions(currentEditingSession, widget); + })); this.chatEditingSessionDisposables.set(currentEditingSession.chatSessionId, disposableStore); } @@ -125,3 +118,44 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben super.dispose(); } } + +export interface IChatRelatedFile { + uri: URI; + description: string; +} +export class ChatRelatedFiles extends Disposable { + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + private _removedFiles = new ResourceSet(); + get removedFiles() { + return this._removedFiles; + } + + private _value: IChatRelatedFile[] = []; + get value() { + return this._value; + } + + set value(value: IChatRelatedFile[]) { + this._value = value; + this._onDidChange.fire(); + } + + remove(uri: URI) { + this._value = this._value.filter(file => !isEqual(file.uri, uri)); + this._removedFiles.add(uri); + this._onDidChange.fire(); + } + + clearRemovedFiles() { + this._removedFiles.clear(); + } + + clear() { + this._value = []; + this._removedFiles.clear(); + this._onDidChange.fire(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/imageUtils.ts b/src/vs/workbench/contrib/chat/browser/imageUtils.ts index e336a86b0f2..b5f426737fd 100644 --- a/src/vs/workbench/contrib/chat/browser/imageUtils.ts +++ b/src/vs/workbench/contrib/chat/browser/imageUtils.ts @@ -80,6 +80,17 @@ export function convertStringToUInt8Array(data: string): Uint8Array { return new TextEncoder().encode(data); } +// Only used for URLs +export function convertUint8ArrayToString(data: Uint8Array): string { + try { + const decoder = new TextDecoder(); + const decodedString = decoder.decode(data); + return decodedString; + } catch { + return ''; + } +} + function isValidBase64(str: string): boolean { // checks if the string is a valid base64 string that is NOT encoded return /^[A-Za-z0-9+/]*={0,2}$/.test(str) && (() => { diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index ab12b8c9462..253d448671d 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -10,11 +10,12 @@ import { CancellationError, isCancellationError } from '../../../../base/common/ import { Emitter } from '../../../../base/common/event.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ChatModel } from '../common/chatModel.js'; import { ChatToolInvocation } from '../common/chatProgressTypes/chatToolInvocation.js'; import { IChatService } from '../common/chatService.js'; @@ -36,7 +37,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private _tools = new Map(); private _toolContextKeys = new Set(); - + private readonly _ctxToolsCount: IContextKey; private _callsByRequestId = new Map(); @@ -56,6 +57,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo this._onDidChangeToolsScheduler.schedule(); } })); + + this._ctxToolsCount = ChatContextKeys.Tools.toolsCount.bindTo(_contextKeyService); } registerToolData(toolData: IToolData): IDisposable { @@ -64,12 +67,14 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } this._tools.set(toolData.id, { data: toolData }); + this._ctxToolsCount.set(this._tools.size); this._onDidChangeToolsScheduler.schedule(); toolData.when?.keys().forEach(key => this._toolContextKeys.add(key)); return toDisposable(() => { this._tools.delete(toolData.id); + this._ctxToolsCount.set(this._tools.size); this._refreshAllToolContextKeys(); this._onDidChangeToolsScheduler.schedule(); }); @@ -267,6 +272,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo super.dispose(); this._callsByRequestId.forEach(calls => dispose(calls)); + this._ctxToolsCount.reset(); } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index a542bb5c97b..56396183cc5 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -435,11 +435,6 @@ have to be updated for changes to the rules above, or to support more deeply nes padding: 8px 20px; } -.interactive-item-container.interactive-item-compact.no-padding { - padding: unset; - gap: unset; -} - .interactive-item-container.interactive-item-compact .header { height: 16px; } @@ -569,11 +564,12 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-editing-session .chat-editing-session-container { - margin-bottom: -14px; + margin-bottom: -13px; padding: 6px 8px 18px 8px; box-sizing: border-box; background-color: var(--vscode-editor-background); border: 1px solid var(--vscode-input-border, transparent); + border-bottom: none; border-radius: 4px; display: flex; flex-direction: column; @@ -692,7 +688,6 @@ have to be updated for changes to the rules above, or to support more deeply nes gap: 4px; margin-top: 6px; flex-wrap: wrap; - overflow: hidden; } .chat-related-files { @@ -700,6 +695,7 @@ have to be updated for changes to the rules above, or to support more deeply nes flex-wrap: wrap; align-items: center; gap: 4px; + max-width: 100%; } .chat-related-files .monaco-button-dropdown .monaco-text-button { @@ -724,8 +720,8 @@ have to be updated for changes to the rules above, or to support more deeply nes border-style: dashed; align-items: center; overflow: hidden; - padding: 0 0 0 2px; gap: 2px; + padding: 0 4px; } .chat-related-files .monaco-button.codicon.codicon-add { @@ -733,9 +729,11 @@ have to be updated for changes to the rules above, or to support more deeply nes flex-direction: column; color: var(--vscode-descriptionForeground); padding-top: 3px; + margin-left: -4px; + padding-left: 4px; font-size: 14px; /* The + codicon is large, make it look more like the x codicon */ height: calc(100% - 3px); - width: 20px; + width: 17px; outline-offset: -2px !important; } @@ -861,13 +859,28 @@ have to be updated for changes to the rules above, or to support more deeply nes display: flex; } -.interactive-session .chat-input-toolbars :first-child { - margin-right: auto; +@container chat-input-container (max-width: 130px) { + .interactive-session .chat-input-toolbars .chat-modelPicker-item { + /* Hides modelpicker when the container width is 130px or less */ + display: none; + } +} + +.interactive-session .interactive-input-part:not(.compact) .chat-input-toolbars > .chat-execute-toolbar { + container-type: inline-size; + container-name: chat-input-container; + flex: 1; + display: flex; + justify-content: flex-end; } .interactive-session .chat-input-toolbars > .chat-execute-toolbar { min-width: 0px; + .monaco-action-bar { + min-width: 0px; + } + .chat-modelPicker-item { min-width: 0px; @@ -890,7 +903,7 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-icon-foreground) !important; } -.interactive-session .chat-input-toolbars .chat-modelPicker-item .action-label { +.interactive-session .chat-input-toolbars .chat-dropdown-item .action-label { height: 16px; padding: 3px 0px 3px 6px; display: flex; @@ -898,7 +911,7 @@ have to be updated for changes to the rules above, or to support more deeply nes } -.interactive-session .chat-input-toolbars .chat-modelPicker-item .action-label .codicon-chevron-down { +.interactive-session .chat-input-toolbars .chat-dropdown-item .action-label .codicon-chevron-down { font-size: 12px; margin-left: 2px; } @@ -1041,14 +1054,6 @@ have to be updated for changes to the rules above, or to support more deeply nes border-style: dashed; opacity: 0.75; } -.chat-attached-context .chat-prompt-attachment .monaco-button { - border-left: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); - margin-left: 3px; -} -.chat-attached-context .chat-prompt-attachment.error .monaco-button, -.chat-attached-context .chat-prompt-attachment.warning .monaco-button { - border-left-color: currentColor; -} .chat-attached-context .chat-prompt-attachment:focus .monaco-button { border-color: var(--vscode-focusBorder); } @@ -1120,12 +1125,20 @@ have to be updated for changes to the rules above, or to support more deeply nes padding: 8px 0 0 0 } +.chat-attachment-toolbar .action-item:not(:last-child) { + margin-right: 4px; +} + .action-item.chat-attached-context-attachment.chat-add-files { height: 20px; - gap: 0px; color: var(--vscode-descriptionForeground); } +.action-item.chat-attached-context-attachment.chat-add-files span.keybinding { + display: none; +} + +.action-item.chat-mcp .action-label, .action-item.chat-attached-context-attachment.chat-add-files .action-label, .interactive-session .chat-attached-context .chat-attached-context-attachment { display: flex; @@ -1143,8 +1156,53 @@ have to be updated for changes to the rules above, or to support more deeply nes .action-item.chat-attached-context-attachment.chat-add-files .action-label { color: var(--vscode-descriptionForeground); font-family: unset; + gap: 5px; } +.action-item.chat-mcp { + display: flex !important; + + &.chat-mcp-has-action .action-label { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: 0; + } + + .chat-mcp-action { + align-self: stretch; + padding: 0 2px; + border-radius: 0; + outline: 0; + border: 0; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + background: var(--vscode-button-background); + cursor: pointer; + + .codicon { + width: fit-content; + color: var(--vscode-button-foreground); + } + + .codicon::before { + font-size: 14px; + } + + &.chat-mcp-action-error { + background: var(--vscode-activityErrorBadge-background); + + .codicon { + color: var(--vscode-activityErrorBadge-foreground); + } + } + } +} + +.quick-input-list .quick-input-list-rows > .quick-input-list-row .monaco-icon-label.tool-pick .codicon[class*='codicon-'] { + font-size: 14px; +} + + .action-item.chat-attached-context-attachment.chat-add-files .action-label.codicon::before { font: normal normal normal 16px/1 codicon; } @@ -1177,8 +1235,11 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container .monaco-highlighted-label { - display: flex !important; + display: block !important; align-items: center !important; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .monaco-button.codicon.codicon-close, @@ -1197,6 +1258,7 @@ have to be updated for changes to the rules above, or to support more deeply nes flex-wrap: wrap; cursor: default; gap: 4px; + max-width: 100%; } .interactive-session .interactive-input-part.compact .chat-attached-context { @@ -1248,11 +1310,11 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label { - gap: 3px; + gap: 4px; } .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label::before { - height: 90%; + height: 1em; width: auto; padding: 0; line-height: 1em !important; @@ -1265,6 +1327,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label.predefined-file-icon::before { padding: 0 0 0 2px; + align-content: center; } .interactive-session .interactive-item-container.interactive-request .chat-attached-context .chat-attached-context-attachment { @@ -1659,6 +1722,11 @@ have to be updated for changes to the rules above, or to support more deeply nes user-select: none; outline: none; border: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + display: inline-block; } .chat-attached-context-attachment.show-file-icons.warning .chat-attached-context-custom-text { @@ -1723,3 +1791,7 @@ have to be updated for changes to the rules above, or to support more deeply nes } } } + +.hideSuggestTextIcons .suggest-widget .monaco-list .monaco-list-row .suggest-icon.codicon-symbol-text::before { + display: none; +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css index 331871acc2f..9356b690797 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css @@ -98,12 +98,16 @@ color: var(--vscode-button-foreground); } -.chat-editor-overlay-widget .monaco-action-bar .action-item.disabled > .action-label.codicon::before, -.chat-editor-overlay-widget .monaco-action-bar .action-item.disabled > .action-label.codicon, -.chat-editor-overlay-widget .monaco-action-bar .action-item.disabled > .action-label, -.chat-editor-overlay-widget .monaco-action-bar .action-item.disabled > .action-label:hover { - color: var(--vscode-button-foreground); - opacity: 0.7; +.chat-diff-change-content-widget .monaco-action-bar .action-item.disabled, +.chat-editor-overlay-widget .monaco-action-bar .action-item.disabled { + + > .action-label.codicon::before, + > .action-label.codicon, + > .action-label, + > .action-label:hover { + color: var(--vscode-button-foreground); + opacity: 0.7; + } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css b/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css index d3e9784d3e2..324e94d42e2 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css @@ -39,6 +39,7 @@ display: inline-block; line-height: 100%; overflow: hidden; + font-size: 100% !important; background-size: contain; background-position: center; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index 911602f593f..82f35aebb20 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -5,11 +5,82 @@ /* Overall */ +.chat-status-bar-entry-tooltip { + margin-top: 4px; + margin-bottom: 4px; +} + .chat-status-bar-entry-tooltip hr { margin-top: 8px; margin-bottom: 8px; } +.chat-status-bar-entry-tooltip div.header { + color: var(--vscode-descriptionForeground); + margin-bottom: 4px; + font-weight: 600; +} + +.chat-status-bar-entry-tooltip div.description { + color: var(--vscode-descriptionForeground); +} + +.chat-status-bar-entry-tooltip .monaco-button { + margin-top: 5px; + margin-bottom: 5px; +} + +/* Setup for New User */ + +.chat-status-bar-entry-tooltip .setup .chat-feature-container { + display: flex; + align-items: center; + gap: 5px; + padding: 4px; +} + +/* Quota Indicator */ + +.chat-status-bar-entry-tooltip .quota-indicator { + margin-bottom: 6px; +} + +.chat-status-bar-entry-tooltip .quota-indicator .quota-label { + display: flex; + justify-content: space-between; + margin-bottom: 3px; +} + +.chat-status-bar-entry-tooltip .quota-indicator .quota-bar { + width: 100%; + height: 4px; + background-color: var(--vscode-gauge-foreground); + border-radius: 4px; + border: 1px solid var(--vscode-gauge-border); +} + +.chat-status-bar-entry-tooltip .quota-indicator .quota-bar .quota-bit { + height: 100%; + background-color: var(--vscode-gauge-background); + border-radius: 4px; +} + +.chat-status-bar-entry-tooltip .quota-indicator.warning .quota-bar { + background-color: var(--vscode-gauge-warningForeground); +} + +.chat-status-bar-entry-tooltip .quota-indicator.warning .quota-bar .quota-bit { + background-color: var(--vscode-gauge-warningBackground); +} + +.chat-status-bar-entry-tooltip .quota-indicator.error .quota-bar { + background-color: var(--vscode-gauge-errorForeground); +} + +.chat-status-bar-entry-tooltip .quota-indicator.error .quota-bar .quota-bit { + background-color: var(--vscode-gauge-errorBackground); +} + /* Settings */ .chat-status-bar-entry-tooltip .settings { @@ -18,58 +89,25 @@ gap: 5px; } +.chat-status-bar-entry-tooltip .settings .setting { + display: flex; + align-items: center; +} + +.chat-status-bar-entry-tooltip .settings .setting .monaco-checkbox { + height: 14px; + width: 14px; + margin-right: 5px; +} + .chat-status-bar-entry-tooltip .settings .setting .codicon { - font-size: 16px; + font-size: 12px; } .chat-status-bar-entry-tooltip .settings .setting .setting-label { cursor: pointer; } -/* Shortcuts */ - -.chat-status-bar-entry-tooltip .shortcuts { - display: flex; - flex-direction: column; - gap: 5px; -} - -.chat-status-bar-entry-tooltip .shortcuts .shortcut { - display: flex; - gap: 10px; -} - -.chat-status-bar-entry-tooltip .shortcuts .shortcut .shortcut-label { - flex: 1; - cursor: pointer; -} - -.chat-status-bar-entry-tooltip .shortcuts .shortcut .monaco-keybinding { - cursor: pointer; -} - -/* Quota Indicator */ - -.chat-status-bar-entry-tooltip .quota-indicator { - margin-top: 8px; - margin-bottom: 8px; -} - -.chat-status-bar-entry-tooltip .quota-indicator .quota-label { - display: flex; - justify-content: space-between; -} - -.chat-status-bar-entry-tooltip .quota-indicator .quota-bar { - width: 100%; - height: 8px; - background-color: var(--vscode-activityBarBadge-foreground); - border-color: var(--vscode-activityBarBadge-background); - border-radius: 6px; -} - -.chat-status-bar-entry-tooltip .quota-indicator .quota-bar .quota-bit { - height: 100%; - background-color: var(--vscode-activityBarBadge-background); - border-radius: 6px; +.chat-status-bar-entry-tooltip .settings .setting.disabled .setting-label { + color: var(--vscode-disabledForeground); } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css index 785aeaa2fa7..0372805a40a 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css @@ -4,11 +4,8 @@ *--------------------------------------------------------------------------------------------*/ .chat-welcome-view .chat-setup-view { - text-align: center; - p { - width: 100%; - } + text-align: center; .chat-features-container { display: flex; @@ -18,6 +15,22 @@ border: 1px solid var(--vscode-chat-requestBorder); background-color: var(--vscode-chat-requestBackground); } +} + +.dialog-message-body .chat-setup-view { + + p.legal { + font-size: 12px; + color: var(--vscode-descriptionForeground); + } +} + +.dialog-message-body .chat-setup-view, +.chat-welcome-view .chat-setup-view { + + p { + width: 100%; + } .chat-feature-container { display: flex; diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts index 28e4f6b52a6..6f73d7ee036 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts @@ -11,13 +11,16 @@ import { askForPromptSourceFolder } from './dialogs/askForPromptSourceFolder.js' import { IFileService } from '../../../../../../../platform/files/common/files.js'; import { ILabelService } from '../../../../../../../platform/label/common/label.js'; import { IOpenerService } from '../../../../../../../platform/opener/common/opener.js'; +import { PromptsConfig } from '../../../../../../../platform/prompts/common/config.js'; import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; import { IPromptPath, IPromptsService } from '../../../../common/promptSyntax/service/types.js'; -import { appendToCommandPalette } from '../../../../../files/browser/fileActions.contribution.js'; import { IQuickInputService } from '../../../../../../../platform/quickinput/common/quickInput.js'; import { ServicesAccessor } from '../../../../../../../platform/instantiation/common/instantiation.js'; import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { MenuId, MenuRegistry } from '../../../../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../../../../../../platform/contextkey/common/contextkey.js'; +import { ChatContextKeys } from '../../../../common/chatContextKeys.js'; /** * Base command ID prefix. @@ -30,9 +33,9 @@ const BASE_COMMAND_ID = 'workbench.command.prompts.create'; const LOCAL_COMMAND_ID = `${BASE_COMMAND_ID}.local`; /** - * Command ID for creating a 'global' prompt. + * Command ID for creating a 'user' prompt. */ -const GLOBAL_COMMAND_ID = `${BASE_COMMAND_ID}.global`; +const USER_COMMAND_ID = `${BASE_COMMAND_ID}.user`; /** * Title of the 'create local prompt' command. @@ -40,9 +43,9 @@ const GLOBAL_COMMAND_ID = `${BASE_COMMAND_ID}.global`; const LOCAL_COMMAND_TITLE = localize('commands.prompts.create.title.local', "Create Prompt"); /** - * Title of the 'create global prompt' command. + * Title of the 'create user prompt' command. */ -const GLOBAL_COMMAND_TITLE = localize('commands.prompts.create.title.global', "Create Global Prompt"); +const USER_COMMAND_TITLE = localize('commands.prompts.create.title.user', "Create User Prompt"); /** * The command implementation. @@ -95,7 +98,7 @@ const command = async ( /** * Factory for creating the command handler with specific prompt `type`. */ -const commandFactory = (type: 'local' | 'global') => { +const commandFactory = (type: 'local' | 'user') => { return async (accessor: ServicesAccessor): Promise => { return command(accessor, type); }; @@ -108,35 +111,39 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: LOCAL_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib, handler: commandFactory('local'), + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), }); /** - * Register the "Create Global Prompt" command. + * Register the "Create User Prompt" command. */ KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: GLOBAL_COMMAND_ID, + id: USER_COMMAND_ID, weight: KeybindingWeight.WorkbenchContrib, - handler: commandFactory('global'), + handler: commandFactory('user'), + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), }); /** * Register the "Create Prompt" command in the command palette. */ -appendToCommandPalette( - { +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { id: LOCAL_COMMAND_ID, title: LOCAL_COMMAND_TITLE, - category: CHAT_CATEGORY, + category: CHAT_CATEGORY }, -); + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) +}); /** - * Register the "Create Global Prompt" command in the command palette. + * Register the "Create User Prompt" command in the command palette. */ -appendToCommandPalette( - { - id: GLOBAL_COMMAND_ID, - title: GLOBAL_COMMAND_TITLE, +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: USER_COMMAND_ID, + title: USER_COMMAND_TITLE, category: CHAT_CATEGORY, }, -); + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) +}); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts index 0c7ff2db72f..cd412256291 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts @@ -11,7 +11,7 @@ import { IQuickInputService } from '../../../../../../../../platform/quickinput/ * Asks the user for a prompt name. */ export const askForPromptName = async ( - _type: 'local' | 'global', + _type: 'local' | 'user', quickInputService: IQuickInputService, ): Promise => { const result = await quickInputService.input( diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts index 68f93da4348..2f446a81472 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptSourceFolder.ts @@ -21,7 +21,7 @@ interface IAskForFolderOptions { /** * Prompt type. */ - readonly type: 'local' | 'global'; + readonly type: 'local' | 'user'; readonly labelService: ILabelService; readonly openerService: IOpenerService; diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts index d449726d917..710637d2fdf 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/utils/createPromptFile.ts @@ -9,8 +9,8 @@ import { assert } from '../../../../../../../../base/common/assert.js'; import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; import { dirname } from '../../../../../../../../base/common/resources.js'; import { IFileService } from '../../../../../../../../platform/files/common/files.js'; -import { isPromptFile, PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js'; import { ICommandService } from '../../../../../../../../platform/commands/common/commands.js'; +import { isPromptFile, PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js'; /** * Options for the {@link createPromptFile} utility. diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts index 776433e6625..87aa3636fe4 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/usePromptCommand.ts @@ -8,24 +8,30 @@ import { URI } from '../../../../../../base/common/uri.js'; import { CHAT_CATEGORY } from '../../actions/chatActions.js'; import { IChatWidget, IChatWidgetService } from '../../chat.js'; import { KeyMod, KeyCode } from '../../../../../../base/common/keyCodes.js'; +import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; +import { IViewsService } from '../../../../../services/views/common/viewsService.js'; import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; -import { appendToCommandPalette } from '../../../../files/browser/fileActions.contribution.js'; import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../../../editor/browser/editorBrowser.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IChatAttachPromptActionOptions, ATTACH_PROMPT_ACTION_ID } from '../../actions/chatAttachPromptAction/chatAttachPromptAction.js'; +import { MenuId, MenuRegistry } from '../../../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { ChatContextKeys } from '../../../common/chatContextKeys.js'; /** * Command ID of the "Use Prompt" command. */ -const COMMAND_ID = 'workbench.command.prompts.use'; +export const COMMAND_ID = 'workbench.command.prompts.use'; /** * Keybinding of the "Use Prompt" command. + * The `cmd + /` is the current keybinding for 'attachment', so we use + * the `alt` key modifier to convey the "prompt attachment" action. */ -const COMMAND_KEY_BINDING = KeyMod.Alt | KeyMod.Shift | KeyCode.KeyE; +const COMMAND_KEY_BINDING = KeyMod.CtrlCmd | KeyCode.Slash | KeyMod.Alt; /** * Implementation of the "Use Prompt" command. The command works in the following way. @@ -49,36 +55,17 @@ const command = async ( accessor: ServicesAccessor, ): Promise => { const commandService = accessor.get(ICommandService); + const viewsService = accessor.get(IViewsService); const options: IChatAttachPromptActionOptions = { resource: getActivePromptUri(accessor), widget: getFocusedChatWidget(accessor), + viewsService, }; await commandService.executeCommand(ATTACH_PROMPT_ACTION_ID, options); }; -/** - * Register the "Use Prompt" command with its keybinding. - */ -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: COMMAND_ID, - weight: KeybindingWeight.WorkbenchContrib, - primary: COMMAND_KEY_BINDING, - handler: command, -}); - -/** - * Register the "Use Prompt" command in the `command palette`. - */ -appendToCommandPalette( - { - id: COMMAND_ID, - title: localize('commands.prompts.use.title', "Use Prompt"), - category: CHAT_CATEGORY, - }, -); - /** * Get chat widget reference to attach prompt to. */ @@ -139,3 +126,26 @@ const getActivePromptUri = ( return undefined; }; + +/** + * Register the "Use Prompt" command with its keybinding. + */ +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + primary: COMMAND_KEY_BINDING, + handler: command, + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled), +}); + +/** + * Register the "Use Prompt" command in the `command palette`. + */ +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: COMMAND_ID, + title: localize('commands.prompts.use.title', "Use Prompt"), + category: CHAT_CATEGORY + }, + when: ContextKeyExpr.and(PromptsConfig.enabledCtx, ChatContextKeys.enabled) +}); diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index eb8eadcfa0d..d4ff4fb01c6 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -7,7 +7,7 @@ import * as dom from '../../../../../base/browser/dom.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Event } from '../../../../../base/common/event.js'; -import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; @@ -17,7 +17,8 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ILogService } from '../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; -import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; +import { IChatAgentService } from '../../common/chatAgents.js'; +import { ChatAgentLocation } from '../../common/constants.js'; import { chatViewsWelcomeRegistry, IChatViewsWelcomeDescriptor } from './chatViewsWelcome.js'; const $ = dom.$; @@ -116,14 +117,14 @@ export interface IChatViewWelcomeContent { export interface IChatViewWelcomeRenderOptions { firstLinkToButton?: boolean; location: ChatAgentLocation; - isWidgetWelcomeViewContent?: boolean; + isWidgetAgentWelcomeViewContent?: boolean; } export class ChatViewWelcomePart extends Disposable { public readonly element: HTMLElement; constructor( - content: IChatViewWelcomeContent, + public readonly content: IChatViewWelcomeContent, options: IChatViewWelcomeRenderOptions | undefined, @IOpenerService private openerService: IOpenerService, @IInstantiationService private instantiationService: IInstantiationService, @@ -147,10 +148,7 @@ export class ChatViewWelcomePart extends Disposable { title.textContent = content.title; // Preview indicator - if (options?.location === ChatAgentLocation.EditingSession && typeof content.message !== 'function' && chatAgentService.toolsAgentModeEnabled && options.isWidgetWelcomeViewContent) { - // Override welcome message for the agent. Sort of a hack, should it come from the participant? This case is different because the welcome content typically doesn't change per ChatWidget - const agentMessage = localize({ key: 'agentMessage', comment: ['{Locked="["}', '{Locked="]({0})"}'] }, "Ask Copilot to edit your files in [agent mode]({0}). Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.\n\nCopilot is powered by AI, so mistakes are possible. Review output carefully before use.", 'https://aka.ms/vscode-copilot-agent'); - content.message = new MarkdownString(agentMessage); + if (typeof content.message !== 'function' && options?.isWidgetAgentWelcomeViewContent) { const container = dom.append(this.element, $('.chat-welcome-view-indicator-container')); dom.append(container, $('.chat-welcome-view-subtitle', undefined, localize('agentModeSubtitle', "Agent Mode"))); dom.append(container, $('.chat-welcome-view-indicator', undefined, localize('experimental', "EXPERIMENTAL"))); diff --git a/src/vs/workbench/contrib/chat/common/chat.ts b/src/vs/workbench/contrib/chat/common/chat.ts new file mode 100644 index 00000000000..7e2b00c0c8f --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chat.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ChatMode } from './constants.js'; + +export function checkModeOption(mode: ChatMode, option: boolean | ((mode: ChatMode) => boolean) | undefined): boolean | undefined { + if (option === undefined) { + return undefined; + } + if (typeof option === 'function') { + return option(mode); + } + return option; +} diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 633010ae07e..a1b5af3eb61 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -25,8 +25,9 @@ import { asJson, IRequestService } from '../../../../platform/request/common/req import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ChatContextKeys } from './chatContextKeys.js'; import { IChatProgressHistoryResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from './chatModel.js'; -import { IRawChatCommandContribution, RawChatParticipantLocation } from './chatParticipantContribTypes.js'; +import { IRawChatCommandContribution } from './chatParticipantContribTypes.js'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from './chatService.js'; +import { ChatAgentLocation, ChatMode } from './constants.js'; //#region agent service, commands etc @@ -36,27 +37,6 @@ export interface IChatAgentHistoryEntry { result: IChatAgentResult; } -export enum ChatAgentLocation { - Panel = 'panel', - Terminal = 'terminal', - Notebook = 'notebook', - Editor = 'editor', - EditingSession = 'editing-session', -} - -export namespace ChatAgentLocation { - export function fromRaw(value: RawChatParticipantLocation | string): ChatAgentLocation { - switch (value) { - case 'panel': return ChatAgentLocation.Panel; - case 'terminal': return ChatAgentLocation.Terminal; - case 'notebook': return ChatAgentLocation.Notebook; - case 'editor': return ChatAgentLocation.Editor; - case 'editing-session': return ChatAgentLocation.EditingSession; - } - return ChatAgentLocation.Panel; - } -} - export interface IChatAgentData { id: string; name: string; @@ -145,6 +125,7 @@ export interface IChatAgentMetadata { followupPlaceholder?: string; isSticky?: boolean; requester?: IChatRequesterInformation; + welcomeMessageContent?: IChatWelcomeMessageContent; } @@ -163,6 +144,7 @@ export interface IChatAgentRequest { acceptedConfirmationData?: any[]; rejectedConfirmationData?: any[]; userSelectedModelId?: string; + userSelectedTools?: string[]; } export interface IChatQuestion { @@ -206,9 +188,7 @@ export interface IChatAgentService { * undefined when an agent was removed */ readonly onDidChangeAgents: Event; - readonly onDidChangeToolsAgentModeEnabled: Event; - readonly toolsAgentModeEnabled: boolean; - toggleToolsAgentMode(enabled?: boolean): void; + readonly hasToolsAgent: boolean; registerAgent(id: string, data: IChatAgentData): IDisposable; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; @@ -231,7 +211,7 @@ export interface IChatAgentService { /** * Get the default agent (only if activated) */ - getDefaultAgent(location: ChatAgentLocation): IChatAgent | undefined; + getDefaultAgent(location: ChatAgentLocation, mode?: ChatMode): IChatAgent | undefined; /** * Get the default agent data that has been contributed (may not be activated yet) @@ -241,8 +221,6 @@ export interface IChatAgentService { updateAgent(id: string, updateMetadata: IChatAgentMetadata): void; } -const ChatToolsAgentModeStorageKey = 'chat.toolsAgentMode'; - export class ChatAgentService extends Disposable implements IChatAgentService { public static readonly AGENT_LEADER = '@'; @@ -254,21 +232,16 @@ export class ChatAgentService extends Disposable implements IChatAgentService { private readonly _onDidChangeAgents = new Emitter(); readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; - private readonly _onDidChangeToolsAgentModeEnabled = new Emitter(); - readonly onDidChangeToolsAgentModeEnabled: Event = this._onDidChangeToolsAgentModeEnabled.event; - private readonly _agentsContextKeys = new Set(); private readonly _hasDefaultAgent: IContextKey; private readonly _defaultAgentRegistered: IContextKey; private readonly _editingAgentRegistered: IContextKey; - private readonly _agentModeContextKey: IContextKey; private readonly _hasToolsAgentContextKey: IContextKey; private _chatParticipantDetectionProviders = new Map(); constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IStorageService private readonly storageService: IStorageService, ) { super(); this._hasDefaultAgent = ChatContextKeys.enabled.bindTo(this.contextKeyService); @@ -280,12 +253,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { } })); - this._agentModeContextKey = ChatContextKeys.Editing.agentMode.bindTo(contextKeyService); this._hasToolsAgentContextKey = ChatContextKeys.Editing.hasToolsAgent.bindTo(contextKeyService); - this._agentModeContextKey.set( - this.storageService.getBoolean(ChatToolsAgentModeStorageKey, StorageScope.WORKSPACE, false)); - this._register( - this.storageService.onWillSaveState(() => this.storageService.store(ChatToolsAgentModeStorageKey, this._agentModeContextKey.get(), StorageScope.WORKSPACE, StorageTarget.USER))); } registerAgent(id: string, data: IChatAgentData): IDisposable { @@ -412,9 +380,13 @@ export class ChatAgentService extends Disposable implements IChatAgentService { this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl)); } - getDefaultAgent(location: ChatAgentLocation): IChatAgent | undefined { + getDefaultAgent(location: ChatAgentLocation, mode?: ChatMode): IChatAgent | undefined { + if (mode === ChatMode.Edit || mode === ChatMode.Agent) { + location = ChatAgentLocation.EditingSession; + } + return findLast(this.getActivatedAgents(), a => { - if (location === ChatAgentLocation.EditingSession && this.toolsAgentModeEnabled !== !!a.isToolsAgent) { + if ((mode === ChatMode.Agent) !== !!a.isToolsAgent) { return false; } @@ -422,14 +394,8 @@ export class ChatAgentService extends Disposable implements IChatAgentService { }); } - public get toolsAgentModeEnabled(): boolean { - return !!this._hasToolsAgentContextKey.get() && !!this._agentModeContextKey.get(); - } - - toggleToolsAgentMode(enabled?: boolean): void { - this._agentModeContextKey.set(enabled ?? !this._agentModeContextKey.get()); - this._onDidChangeToolsAgentModeEnabled.fire(); - this._onDidChangeAgents.fire(this.getDefaultAgent(ChatAgentLocation.EditingSession)); + public get hasToolsAgent(): boolean { + return !!this._hasToolsAgentContextKey.get(); } getContributedDefaultAgent(location: ChatAgentLocation): IChatAgentData | undefined { @@ -784,3 +750,4 @@ export function reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAge return revive(agent); } +export { ChatAgentLocation }; diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 591bbdcad92..19b59301b57 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -7,7 +7,7 @@ import { localize } from '../../../../nls.js'; import { ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js'; import { RemoteNameContext } from '../../../common/contextkeys.js'; -import { ChatAgentLocation } from './chatAgents.js'; +import { ChatAgentLocation, ChatConfiguration, ChatMode } from './constants.js'; export namespace ChatContextKeys { export const responseVote = new RawContextKey('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); @@ -30,7 +30,9 @@ export namespace ChatContextKeys { export const inputHasFocus = new RawContextKey('chatInputHasFocus', false, { type: 'boolean', description: localize('interactiveInputHasFocus', "True when the chat input has focus.") }); export const inChatInput = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); + export const inUnifiedChat = new RawContextKey('inUnifiedChat', false, { type: 'boolean', description: localize('inUnifiedChat', "True when focus is in the unified chat widget, false otherwise.") }); export const instructionsAttached = new RawContextKey('chatInstructionsAttached', false, { type: 'boolean', description: localize('chatInstructionsAttachedContextDescription', "True when the chat has a prompt instructions attached.") }); + export const chatMode = new RawContextKey('chatMode', ChatMode.Chat, { type: 'string', description: localize('chatMode', "The current chat mode.") }); export const supported = ContextKeyExpr.or(IsWebContext.toNegated(), RemoteNameContext.notEqualsTo('')); // supported on desktop and in web only with a remote connection export const enabled = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); @@ -49,30 +51,31 @@ export namespace ChatContextKeys { export const languageModelsAreUserSelectable = new RawContextKey('chatModelsAreUserSelectable', false, { type: 'boolean', description: localize('chatModelsAreUserSelectable', "True when the chat model can be selected manually by the user.") }); export const Setup = { - - // State - signedOut: new RawContextKey('chatSetupSignedOut', false, true), // True when user is signed out. hidden: new RawContextKey('chatSetupHidden', false, true), // True when chat setup is explicitly hidden. installed: new RawContextKey('chatSetupInstalled', false, true), // True when the chat extension is installed. + fromDialog: ContextKeyExpr.has('config.chat.experimental.setupFromDialog'), + }; - // Plans + export const Entitlement = { + signedOut: new RawContextKey('chatSetupSignedOut', false, true), // True when user is signed out. canSignUp: new RawContextKey('chatPlanCanSignUp', false, true), // True when user can sign up to be a chat limited user. limited: new RawContextKey('chatPlanLimited', false, true), // True when user is a chat limited user. pro: new RawContextKey('chatPlanPro', false, true) // True when user is a chat pro user. }; - export const SetupViewKeys = new Set([ChatContextKeys.Setup.hidden.key, ChatContextKeys.Setup.installed.key, ChatContextKeys.Setup.signedOut.key, ChatContextKeys.Setup.canSignUp.key]); + export const SetupViewKeys = new Set([ChatContextKeys.Setup.hidden.key, ChatContextKeys.Setup.installed.key, ChatContextKeys.Entitlement.signedOut.key, ChatContextKeys.Entitlement.canSignUp.key, ...Setup.fromDialog.keys()]); export const SetupViewCondition = ContextKeyExpr.or( ContextKeyExpr.and( ChatContextKeys.Setup.hidden.negate(), - ChatContextKeys.Setup.installed.negate() + ChatContextKeys.Setup.installed.negate(), + Setup.fromDialog.negate() ), ContextKeyExpr.and( - ChatContextKeys.Setup.canSignUp, + ChatContextKeys.Entitlement.canSignUp, ChatContextKeys.Setup.installed ), ContextKeyExpr.and( - ChatContextKeys.Setup.signedOut, + ChatContextKeys.Entitlement.signedOut, ChatContextKeys.Setup.installed ) )!; @@ -82,7 +85,29 @@ export namespace ChatContextKeys { export const Editing = { hasToolsAgent: new RawContextKey('chatHasToolsAgent', false, { type: 'boolean', description: localize('chatEditingHasToolsAgent', "True when a tools agent is registered.") }), - agentMode: new RawContextKey('chatAgentMode', false, { type: 'boolean', description: localize('chatEditingAgentMode', "True when edits is in agent mode.") }), agentModeDisallowed: new RawContextKey('chatAgentModeDisallowed', undefined, { type: 'boolean', description: localize('chatAgentModeDisallowed', "True when agent mode is not allowed.") }), // experiment-driven disablement + hasToolConfirmation: new RawContextKey('chatHasToolConfirmation', false, { type: 'boolean', description: localize('chatEditingHasToolConfirmation', "True when a tool confirmation is present.") }), + }; + + export const Tools = { + + toolsCount: new RawContextKey('toolsCount', 0, { type: 'number', description: localize('toolsCount', "The count of tools available in the chat.") }) }; } + +export namespace ChatContextKeyExprs { + export const unifiedChatEnabled = ContextKeyExpr.has(`config.${ChatConfiguration.UnifiedChatView}`); + + export const inEditsOrUnified = ContextKeyExpr.or( + ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), + ChatContextKeys.inUnifiedChat); + + export const inNonUnifiedPanel = ContextKeyExpr.and( + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), + ChatContextKeys.inUnifiedChat.negate()); + + export const inEditingMode = ContextKeyExpr.or( + ChatContextKeys.chatMode.isEqualTo(ChatMode.Edit), + ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent), + ); +} diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 48431090b77..0de480c9d88 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -6,7 +6,6 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../base/common/map.js'; import { IObservable, IReader, ITransaction } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { TextEdit } from '../../../../editor/common/languages.js'; @@ -68,7 +67,6 @@ export interface IChatRelatedFilesProvider { export interface WorkingSetDisplayMetadata { state: WorkingSetEntryState; description?: string; - isMarkedReadonly?: boolean; } export interface IStreamingEdits { @@ -88,11 +86,8 @@ export interface IChatEditingSession extends IDisposable { readonly onDidDispose: Event; readonly state: IObservable; readonly entries: IObservable; - readonly workingSet: ResourceMap; - addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Suggested): void; show(): Promise; remove(reason: WorkingSetEntryRemovalReason, ...uris: URI[]): void; - markIsReadonly(uri: URI, isReadonly?: boolean): void; accept(...uris: URI[]): Promise; reject(...uris: URI[]): Promise; getEntry(uri: URI): IModifiedFileEntry | undefined; @@ -157,9 +152,8 @@ export const enum WorkingSetEntryState { Accepted, Rejected, Transient, // TODO@joyceerhl remove this - Attached, + Attached, // TODO@joyceerhl remove this Sent, // TODO@joyceerhl remove this - Suggested, } export const enum ChatEditingSessionChangeType { @@ -262,7 +256,6 @@ export const enum ChatEditingSessionState { export const CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME = 'chat-editing-multi-diff-source'; export const chatEditingWidgetFileStateContextKey = new RawContextKey('chatEditingWidgetFileState', undefined, localize('chatEditingWidgetFileState', "The current state of the file in the chat editing widget")); -export const chatEditingWidgetFileReadonlyContextKey = new RawContextKey('chatEditingWidgetFileReadonly', undefined, localize('chatEditingWidgetFileReadonly', "Whether the file has been marked as read-only in the chat editing widget")); export const chatEditingAgentSupportsReadonlyReferencesContextKey = new RawContextKey('chatEditingAgentSupportsReadonlyReferences', undefined, localize('chatEditingAgentSupportsReadonlyReferences', "Whether the chat editing agent supports readonly references (temporary)")); export const decidedChatEditingResourceContextKey = new RawContextKey('decidedChatEditingResource', []); export const chatEditingResourceContextKey = new RawContextKey('chatEditingResource', undefined); diff --git a/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts b/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts new file mode 100644 index 00000000000..ab6ffdb0324 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatEntitlementService.ts @@ -0,0 +1,882 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import product from '../../../../platform/product/common/product.js'; +import { Barrier } from '../../../../base/common/async.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Lazy } from '../../../../base/common/lazy.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IRequestContext } from '../../../../base/parts/request/common/request.js'; +import { localize } from '../../../../nls.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { asText, IRequestService } from '../../../../platform/request/common/request.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { AuthenticationSession, IAuthenticationExtensionsService, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; +import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; +import { IExtension, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; +import { ChatContextKeys } from './chatContextKeys.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { URI } from '../../../../base/common/uri.js'; +import Severity from '../../../../base/common/severity.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { isWeb } from '../../../../base/common/platform.js'; + +export const IChatEntitlementService = createDecorator('chatEntitlementService'); + +export enum ChatEntitlement { + /** Signed out */ + Unknown = 1, + /** Signed in but not yet resolved */ + Unresolved, + /** Signed in and entitled to Limited */ + Available, + /** Signed in but not entitled to Limited */ + Unavailable, + /** Signed-up to Limited */ + Limited, + /** Signed-up to Pro */ + Pro +} + +export enum ChatSentiment { + /** Out of the box value */ + Standard = 1, + /** Explicitly disabled/hidden by user */ + Disabled = 2, + /** Extensions installed */ + Installed = 3 +} + +export interface IChatQuotas { + readonly chatQuotaExceeded: boolean; + readonly completionsQuotaExceeded: boolean; + readonly quotaResetDate: Date | undefined; + + readonly chatTotal?: number; + readonly completionsTotal?: number; + + readonly chatRemaining?: number; + readonly completionsRemaining?: number; +} + +export interface IChatEntitlementService { + + _serviceBrand: undefined; + + readonly onDidChangeEntitlement: Event; + + readonly entitlement: ChatEntitlement; + + readonly onDidChangeQuotaExceeded: Event; + readonly onDidChangeQuotaRemaining: Event; + + readonly quotas: IChatQuotas; + + update(token: CancellationToken): Promise; + + readonly onDidChangeSentiment: Event; + + readonly sentiment: ChatSentiment; +} + +//#region Service Implementation + +const defaultChat = { + extensionId: product.defaultChatAgent?.extensionId ?? '', + chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '', + upgradePlanUrl: product.defaultChatAgent?.upgradePlanUrl ?? '', + providerId: product.defaultChatAgent?.providerId ?? '', + enterpriseProviderId: product.defaultChatAgent?.enterpriseProviderId ?? '', + providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], + entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', + entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '', + completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '', + chatQuotaExceededContext: product.defaultChatAgent?.chatQuotaExceededContext ?? '', + completionsQuotaExceededContext: product.defaultChatAgent?.completionsQuotaExceededContext ?? '' +}; + +interface IChatQuotasAccessor { + clearQuotas(): void; + acceptQuotas(quotas: IChatQuotas): void; +} + +export class ChatEntitlementService extends Disposable implements IChatEntitlementService { + + declare _serviceBrand: undefined; + + readonly context: Lazy | undefined; + readonly requests: Lazy | undefined; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @IProductService productService: IProductService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + super(); + + this.chatQuotaExceededContextKey = ChatContextKeys.chatQuotaExceeded.bindTo(this.contextKeyService); + this.completionsQuotaExceededContextKey = ChatContextKeys.completionsQuotaExceeded.bindTo(this.contextKeyService); + + this.onDidChangeEntitlement = Event.map( + Event.filter( + this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([ + ChatContextKeys.Entitlement.pro.key, + ChatContextKeys.Entitlement.limited.key, + ChatContextKeys.Entitlement.canSignUp.key, + ChatContextKeys.Entitlement.signedOut.key + ])), this._store + ), () => { }, this._store + ); + + this.onDidChangeSentiment = Event.map( + Event.filter( + this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([ + ChatContextKeys.Setup.hidden.key, + ChatContextKeys.Setup.installed.key + ])), this._store + ), () => { }, this._store + ); + + if ( + !productService.defaultChatAgent || // needs product config + (isWeb && !environmentService.remoteAuthority) // only enabled locally or a remote backend + ) { + ChatContextKeys.Setup.hidden.bindTo(this.contextKeyService).set(true); // hide copilot UI + return; + } + + const context = this.context = new Lazy(() => this._register(instantiationService.createInstance(ChatEntitlementContext))); + this.requests = new Lazy(() => this._register(instantiationService.createInstance(ChatEntitlementRequests, context.value, { + clearQuotas: () => this.clearQuotas(), + acceptQuotas: quotas => this.acceptQuotas(quotas) + }))); + + this.registerListeners(); + } + + //#region --- Entitlements + + readonly onDidChangeEntitlement: Event; + + get entitlement(): ChatEntitlement { + if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Entitlement.pro.key) === true) { + return ChatEntitlement.Pro; + } else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Entitlement.limited.key) === true) { + return ChatEntitlement.Limited; + } else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Entitlement.canSignUp.key) === true) { + return ChatEntitlement.Available; + } else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Entitlement.signedOut.key) === true) { + return ChatEntitlement.Unknown; + } + + return ChatEntitlement.Unresolved; + } + + //#endregion + + //#region --- Quotas + + private readonly _onDidChangeQuotaExceeded = this._register(new Emitter()); + readonly onDidChangeQuotaExceeded = this._onDidChangeQuotaExceeded.event; + + private readonly _onDidChangeQuotaRemaining = this._register(new Emitter()); + readonly onDidChangeQuotaRemaining = this._onDidChangeQuotaRemaining.event; + + private _quotas: IChatQuotas = { chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: undefined }; + get quotas() { return this._quotas; } + + private readonly chatQuotaExceededContextKey: IContextKey; + private readonly completionsQuotaExceededContextKey: IContextKey; + + private ExtensionQuotaContextKeys = { + chatQuotaExceeded: defaultChat.chatQuotaExceededContext, + completionsQuotaExceeded: defaultChat.completionsQuotaExceededContext, + }; + + private registerListeners(): void { + const chatQuotaExceededSet = new Set([this.ExtensionQuotaContextKeys.chatQuotaExceeded]); + const completionsQuotaExceededSet = new Set([this.ExtensionQuotaContextKeys.completionsQuotaExceeded]); + + this._register(this.contextKeyService.onDidChangeContext(e => { + let changed = false; + if (e.affectsSome(chatQuotaExceededSet)) { + const newChatQuotaExceeded = this.contextKeyService.getContextKeyValue(this.ExtensionQuotaContextKeys.chatQuotaExceeded); + if (typeof newChatQuotaExceeded === 'boolean' && newChatQuotaExceeded !== this._quotas.chatQuotaExceeded) { + this._quotas = { + ...this._quotas, + chatQuotaExceeded: newChatQuotaExceeded, + }; + changed = true; + } + } + + if (e.affectsSome(completionsQuotaExceededSet)) { + const newCompletionsQuotaExceeded = this.contextKeyService.getContextKeyValue(this.ExtensionQuotaContextKeys.completionsQuotaExceeded); + if (typeof newCompletionsQuotaExceeded === 'boolean' && newCompletionsQuotaExceeded !== this._quotas.completionsQuotaExceeded) { + this._quotas = { + ...this._quotas, + completionsQuotaExceeded: newCompletionsQuotaExceeded, + }; + changed = true; + } + } + + if (changed) { + this.updateContextKeys(); + this._onDidChangeQuotaExceeded.fire(); + } + })); + } + + acceptQuotas(quotas: IChatQuotas): void { + const oldQuota = this._quotas; + this._quotas = quotas; + this.updateContextKeys(); + + if ( + oldQuota.chatQuotaExceeded !== this._quotas.chatQuotaExceeded || + oldQuota.completionsQuotaExceeded !== this._quotas.completionsQuotaExceeded + ) { + this._onDidChangeQuotaExceeded.fire(); + } + + if ( + oldQuota.chatRemaining !== this._quotas.chatRemaining || + oldQuota.completionsRemaining !== this._quotas.completionsRemaining + ) { + this._onDidChangeQuotaRemaining.fire(); + } + } + + clearQuotas(): void { + if (this.quotas.chatQuotaExceeded || this.quotas.completionsQuotaExceeded) { + this.acceptQuotas({ chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: undefined }); + } + } + + private updateContextKeys(): void { + this.chatQuotaExceededContextKey.set(this._quotas.chatQuotaExceeded); + this.completionsQuotaExceededContextKey.set(this._quotas.completionsQuotaExceeded); + } + + //#endregion + + //#region --- Sentiment + + private readonly _onDidChangeSentiment = this._register(new Emitter()); + readonly onDidChangeSentiment = this._onDidChangeSentiment.event; + + get sentiment(): ChatSentiment { + if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.installed.key) === true) { + return ChatSentiment.Installed; + } else if (this.contextKeyService.getContextKeyValue(ChatContextKeys.Setup.hidden.key) === true) { + return ChatSentiment.Disabled; + } + + return ChatSentiment.Standard; + } + + //#endregion + + async update(token: CancellationToken): Promise { + await this.requests?.value.forceResolveEntitlement(undefined, token); + } +} + +//#endregion + +//#region Chat Entitlement Request Service + +type EntitlementClassification = { + tid: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight'; comment: 'The anonymized analytics id returned by the service'; endpoint: 'GoogleAnalyticsId' }; + entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; + quotaChat: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; + quotaCompletions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of chat completions available to the user' }; + quotaResetDate: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The date the quota will reset' }; + owner: 'bpasero'; + comment: 'Reporting chat entitlements'; +}; + +type EntitlementEvent = { + entitlement: ChatEntitlement; + tid: string; + quotaChat: number | undefined; + quotaCompletions: number | undefined; + quotaResetDate: string | undefined; +}; + +interface IEntitlementsResponse { + readonly access_type_sku: string; + readonly assigned_date: string; + readonly can_signup_for_limited: boolean; + readonly chat_enabled: boolean; + readonly analytics_tracking_id: string; + readonly limited_user_quotas?: { + readonly chat: number; + readonly completions: number; + }; + readonly monthly_quotas?: { + readonly chat: number; + readonly completions: number; + }; + readonly limited_user_reset_date: string; +} + +interface IEntitlements { + readonly entitlement: ChatEntitlement; + readonly quotas?: IQuotas; +} + +interface IQuotas { + readonly chatTotal?: number; + readonly completionsTotal?: number; + + readonly chatRemaining?: number; + readonly completionsRemaining?: number; + + readonly resetDate?: string; +} + +export class ChatEntitlementRequests extends Disposable { + + static providerId(configurationService: IConfigurationService): string { + if (configurationService.getValue(`${defaultChat.completionsAdvancedSetting}.authProvider`) === defaultChat.enterpriseProviderId) { + return defaultChat.enterpriseProviderId; + } + + return defaultChat.providerId; + } + + private state: IEntitlements; + + private pendingResolveCts = new CancellationTokenSource(); + private didResolveEntitlements = false; + + constructor( + private readonly context: ChatEntitlementContext, + private readonly chatQuotasAccessor: IChatQuotasAccessor, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @ILogService private readonly logService: ILogService, + @IRequestService private readonly requestService: IRequestService, + @IDialogService private readonly dialogService: IDialogService, + @IOpenerService private readonly openerService: IOpenerService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService, + ) { + super(); + + this.state = { entitlement: this.context.state.entitlement }; + + this.registerListeners(); + + this.resolve(); + } + + private registerListeners(): void { + this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.resolve())); + + this._register(this.authenticationService.onDidChangeSessions(e => { + if (e.providerId === ChatEntitlementRequests.providerId(this.configurationService)) { + this.resolve(); + } + })); + + this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => { + if (e.id === ChatEntitlementRequests.providerId(this.configurationService)) { + this.resolve(); + } + })); + + this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => { + if (e.id === ChatEntitlementRequests.providerId(this.configurationService)) { + this.resolve(); + } + })); + + this._register(this.context.onDidChange(() => { + if (!this.context.state.installed || this.context.state.entitlement === ChatEntitlement.Unknown) { + // When the extension is not installed or the user is not entitled + // make sure to clear quotas so that any indicators are also gone + this.state = { entitlement: this.state.entitlement, quotas: undefined }; + this.chatQuotasAccessor.clearQuotas(); + } + })); + } + + private async resolve(): Promise { + this.pendingResolveCts.dispose(true); + const cts = this.pendingResolveCts = new CancellationTokenSource(); + + const session = await this.findMatchingProviderSession(cts.token); + if (cts.token.isCancellationRequested) { + return; + } + + // Immediately signal whether we have a session or not + let state: IEntitlements | undefined = undefined; + if (session) { + // Do not overwrite any state we have already + if (this.state.entitlement === ChatEntitlement.Unknown) { + state = { entitlement: ChatEntitlement.Unresolved }; + } + } else { + this.didResolveEntitlements = false; // reset so that we resolve entitlements fresh when signed in again + state = { entitlement: ChatEntitlement.Unknown }; + } + if (state) { + this.update(state); + } + + if (session && !this.didResolveEntitlements) { + // Afterwards resolve entitlement with a network request + // but only unless it was not already resolved before. + await this.resolveEntitlement(session, cts.token); + } + } + + private async findMatchingProviderSession(token: CancellationToken): Promise { + const sessions = await this.doGetSessions(ChatEntitlementRequests.providerId(this.configurationService)); + if (token.isCancellationRequested) { + return undefined; + } + + for (const session of sessions) { + for (const scopes of defaultChat.providerScopes) { + if (this.scopesMatch(session.scopes, scopes)) { + return session; + } + } + } + + return undefined; + } + + private async doGetSessions(providerId: string): Promise { + try { + return await this.authenticationService.getSessions(providerId); + } catch (error) { + // ignore - errors can throw if a provider is not registered + } + + return []; + } + + private scopesMatch(scopes: ReadonlyArray, expectedScopes: string[]): boolean { + return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope)); + } + + private async resolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { + const entitlements = await this.doResolveEntitlement(session, token); + if (typeof entitlements?.entitlement === 'number' && !token.isCancellationRequested) { + this.didResolveEntitlements = true; + this.update(entitlements); + } + + return entitlements; + } + + private async doResolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { + if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.enterpriseProviderId) { + this.logService.trace('[chat entitlement]: enterprise provider, assuming Pro'); + return { entitlement: ChatEntitlement.Pro }; + } + + if (token.isCancellationRequested) { + return undefined; + } + + const response = await this.request(defaultChat.entitlementUrl, 'GET', undefined, session, token); + if (token.isCancellationRequested) { + return undefined; + } + + if (!response) { + this.logService.trace('[chat entitlement]: no response'); + return { entitlement: ChatEntitlement.Unresolved }; + } + + if (response.res.statusCode && response.res.statusCode !== 200) { + this.logService.trace(`[chat entitlement]: unexpected status code ${response.res.statusCode}`); + return { entitlement: ChatEntitlement.Unresolved }; + } + + let responseText: string | null = null; + try { + responseText = await asText(response); + } catch (error) { + // ignore - handled below + } + if (token.isCancellationRequested) { + return undefined; + } + + if (!responseText) { + this.logService.trace('[chat entitlement]: response has no content'); + return { entitlement: ChatEntitlement.Unresolved }; + } + + let entitlementsResponse: IEntitlementsResponse; + try { + entitlementsResponse = JSON.parse(responseText); + this.logService.trace(`[chat entitlement]: parsed result is ${JSON.stringify(entitlementsResponse)}`); + } catch (err) { + this.logService.trace(`[chat entitlement]: error parsing response (${err})`); + return { entitlement: ChatEntitlement.Unresolved }; + } + + let entitlement: ChatEntitlement; + if (entitlementsResponse.access_type_sku === 'free_limited_copilot') { + entitlement = ChatEntitlement.Limited; + } else if (entitlementsResponse.can_signup_for_limited) { + entitlement = ChatEntitlement.Available; + } else if (entitlementsResponse.chat_enabled) { + entitlement = ChatEntitlement.Pro; + } else { + entitlement = ChatEntitlement.Unavailable; + } + + const chatRemaining = entitlementsResponse.limited_user_quotas?.chat; + const completionsRemaining = entitlementsResponse.limited_user_quotas?.completions; + + const entitlements: IEntitlements = { + entitlement, + quotas: { + chatTotal: entitlementsResponse.monthly_quotas?.chat, + completionsTotal: entitlementsResponse.monthly_quotas?.completions, + chatRemaining: typeof chatRemaining === 'number' ? Math.max(0, chatRemaining) : undefined, + completionsRemaining: typeof completionsRemaining === 'number' ? Math.max(0, completionsRemaining) : undefined, + resetDate: entitlementsResponse.limited_user_reset_date + } + }; + + this.logService.trace(`[chat entitlement]: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`); + this.telemetryService.publicLog2('chatInstallEntitlement', { + entitlement: entitlements.entitlement, + tid: entitlementsResponse.analytics_tracking_id, + quotaChat: entitlementsResponse.limited_user_quotas?.chat, + quotaCompletions: entitlementsResponse.limited_user_quotas?.completions, + quotaResetDate: entitlementsResponse.limited_user_reset_date + }); + + return entitlements; + } + + private async request(url: string, type: 'GET', body: undefined, session: AuthenticationSession, token: CancellationToken): Promise; + private async request(url: string, type: 'POST', body: object, session: AuthenticationSession, token: CancellationToken): Promise; + private async request(url: string, type: 'GET' | 'POST', body: object | undefined, session: AuthenticationSession, token: CancellationToken): Promise { + try { + return await this.requestService.request({ + type, + url, + data: type === 'POST' ? JSON.stringify(body) : undefined, + disableCache: true, + headers: { + 'Authorization': `Bearer ${session.accessToken}` + } + }, token); + } catch (error) { + if (!token.isCancellationRequested) { + this.logService.error(`[chat entitlement] request: error ${error}`); + } + + return undefined; + } + } + + private update(state: IEntitlements): void { + this.state = state; + + this.context.update({ entitlement: this.state.entitlement }); + + if (state.quotas) { + this.chatQuotasAccessor.acceptQuotas({ + chatQuotaExceeded: typeof state.quotas.chatRemaining === 'number' ? state.quotas.chatRemaining <= 0 : false, + completionsQuotaExceeded: typeof state.quotas.completionsRemaining === 'number' ? state.quotas.completionsRemaining <= 0 : false, + quotaResetDate: state.quotas.resetDate ? new Date(state.quotas.resetDate) : undefined, + chatTotal: state.quotas.chatTotal, + completionsTotal: state.quotas.completionsTotal, + chatRemaining: state.quotas.chatRemaining, + completionsRemaining: state.quotas.completionsRemaining + }); + } + } + + async forceResolveEntitlement(session: AuthenticationSession | undefined, token = CancellationToken.None): Promise { + if (!session) { + session = await this.findMatchingProviderSession(token); + } + + if (!session) { + return undefined; + } + + return this.resolveEntitlement(session, token); + } + + async signUpLimited(session: AuthenticationSession): Promise { + const body = { + restricted_telemetry: this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? 'disabled' : 'enabled', + public_code_suggestions: 'enabled' + }; + + const response = await this.request(defaultChat.entitlementSignupLimitedUrl, 'POST', body, session, CancellationToken.None); + if (!response) { + const retry = await this.onUnknownSignUpError(localize('signUpNoResponseError', "No response received."), '[chat entitlement] sign-up: no response'); + return retry ? this.signUpLimited(session) : { errorCode: 1 }; + } + + if (response.res.statusCode && response.res.statusCode !== 200) { + if (response.res.statusCode === 422) { + try { + const responseText = await asText(response); + if (responseText) { + const responseError: { message: string } = JSON.parse(responseText); + if (typeof responseError.message === 'string' && responseError.message) { + this.onUnprocessableSignUpError(`[chat entitlement] sign-up: unprocessable entity (${responseError.message})`, responseError.message); + return { errorCode: response.res.statusCode }; + } + } + } catch (error) { + // ignore - handled below + } + } + const retry = await this.onUnknownSignUpError(localize('signUpUnexpectedStatusError', "Unexpected status code {0}.", response.res.statusCode), `[chat entitlement] sign-up: unexpected status code ${response.res.statusCode}`); + return retry ? this.signUpLimited(session) : { errorCode: response.res.statusCode }; + } + + let responseText: string | null = null; + try { + responseText = await asText(response); + } catch (error) { + // ignore - handled below + } + + if (!responseText) { + const retry = await this.onUnknownSignUpError(localize('signUpNoResponseContentsError', "Response has no contents."), '[chat entitlement] sign-up: response has no content'); + return retry ? this.signUpLimited(session) : { errorCode: 2 }; + } + + let parsedResult: { subscribed: boolean } | undefined = undefined; + try { + parsedResult = JSON.parse(responseText); + this.logService.trace(`[chat entitlement] sign-up: response is ${responseText}`); + } catch (err) { + const retry = await this.onUnknownSignUpError(localize('signUpInvalidResponseError', "Invalid response contents."), `[chat entitlement] sign-up: error parsing response (${err})`); + return retry ? this.signUpLimited(session) : { errorCode: 3 }; + } + + // We have made it this far, so the user either did sign-up or was signed-up already. + // That is, because the endpoint throws in all other case according to Patrick. + this.update({ entitlement: ChatEntitlement.Limited }); + + return Boolean(parsedResult?.subscribed); + } + + private async onUnknownSignUpError(detail: string, logMessage: string): Promise { + this.logService.error(logMessage); + + const { confirmed } = await this.dialogService.confirm({ + type: Severity.Error, + message: localize('unknownSignUpError', "An error occurred while signing up for Copilot Free. Would you like to try again?"), + detail, + primaryButton: localize('retry', "Retry") + }); + + return confirmed; + } + + private onUnprocessableSignUpError(logMessage: string, logDetails: string): void { + this.logService.error(logMessage); + + this.dialogService.prompt({ + type: Severity.Error, + message: localize('unprocessableSignUpError', "An error occurred while signing up for Copilot Free."), + detail: logDetails, + buttons: [ + { + label: localize('ok', "OK"), + run: () => { /* noop */ } + }, + { + label: localize('learnMore', "Learn More"), + run: () => this.openerService.open(URI.parse(defaultChat.upgradePlanUrl)) + } + ] + }); + } + + async signIn() { + const providerId = ChatEntitlementRequests.providerId(this.configurationService); + const session = await this.authenticationService.createSession(providerId, defaultChat.providerScopes[0]); + + this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account); + this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, providerId, session.account); + + const entitlements = await this.forceResolveEntitlement(session); + + return { session, entitlements }; + } + + override dispose(): void { + this.pendingResolveCts.dispose(true); + + super.dispose(); + } +} + +//#endregion + +//#region Context + +export interface IChatEntitlementContextState { + entitlement: ChatEntitlement; + hidden?: boolean; + installed?: boolean; + registered?: boolean; +} + +export class ChatEntitlementContext extends Disposable { + + private static readonly CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY = 'chat.setupContext'; + + private readonly canSignUpContextKey: IContextKey; + private readonly signedOutContextKey: IContextKey; + private readonly limitedContextKey: IContextKey; + private readonly proContextKey: IContextKey; + private readonly hiddenContext: IContextKey; + private readonly installedContext: IContextKey; + + private _state: IChatEntitlementContextState; + private suspendedState: IChatEntitlementContextState | undefined = undefined; + get state(): IChatEntitlementContextState { + return this.suspendedState ?? this._state; + } + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + private updateBarrier: Barrier | undefined = undefined; + + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @IStorageService private readonly storageService: IStorageService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @ILogService private readonly logService: ILogService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + ) { + super(); + + this.canSignUpContextKey = ChatContextKeys.Entitlement.canSignUp.bindTo(contextKeyService); + this.signedOutContextKey = ChatContextKeys.Entitlement.signedOut.bindTo(contextKeyService); + this.limitedContextKey = ChatContextKeys.Entitlement.limited.bindTo(contextKeyService); + this.proContextKey = ChatContextKeys.Entitlement.pro.bindTo(contextKeyService); + this.hiddenContext = ChatContextKeys.Setup.hidden.bindTo(contextKeyService); + this.installedContext = ChatContextKeys.Setup.installed.bindTo(contextKeyService); + + this._state = this.storageService.getObject(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, StorageScope.PROFILE) ?? { entitlement: ChatEntitlement.Unknown }; + + this.checkExtensionInstallation(); + this.updateContextSync(); + } + + private async checkExtensionInstallation(): Promise { + + // Await extensions to be ready to be queried + await this.extensionsWorkbenchService.queryLocal(); + + // Listen to change and process extensions once + this._register(Event.runAndSubscribe(this.extensionsWorkbenchService.onChange, e => { + if (e && !ExtensionIdentifier.equals(e.identifier.id, defaultChat.extensionId)) { + return; // unrelated event + } + + const defaultChatExtension = this.extensionsWorkbenchService.local.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); + this.update({ installed: !!defaultChatExtension?.local && this.extensionEnablementService.isEnabled(defaultChatExtension.local) }); + })); + } + + update(context: { installed: boolean }): Promise; + update(context: { hidden: boolean }): Promise; + update(context: { entitlement: ChatEntitlement }): Promise; + update(context: { installed?: boolean; hidden?: boolean; entitlement?: ChatEntitlement }): Promise { + this.logService.trace(`[chat entitlement context] update(): ${JSON.stringify(context)}`); + + if (typeof context.installed === 'boolean') { + this._state.installed = context.installed; + + if (context.installed) { + context.hidden = false; // allows to fallback if the extension is uninstalled + } + } + + if (typeof context.hidden === 'boolean') { + this._state.hidden = context.hidden; + } + + if (typeof context.entitlement === 'number') { + this._state.entitlement = context.entitlement; + + if (this._state.entitlement === ChatEntitlement.Limited || this._state.entitlement === ChatEntitlement.Pro) { + this._state.registered = true; + } else if (this._state.entitlement === ChatEntitlement.Available) { + this._state.registered = false; // only reset when signed-in user can sign-up for limited + } + } + + this.storageService.store(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, this._state, StorageScope.PROFILE, StorageTarget.MACHINE); + + return this.updateContext(); + } + + private async updateContext(): Promise { + await this.updateBarrier?.wait(); + + this.updateContextSync(); + } + + private updateContextSync(): void { + this.logService.trace(`[chat entitlement context] updateContext(): ${JSON.stringify(this._state)}`); + + if (!this._state.hidden && !this._state.installed) { + // this is ugly but fixes flicker from a previous chat install + this.storageService.remove('chat.welcomeMessageContent.panel', StorageScope.APPLICATION); + this.storageService.remove('interactive.sessions', this.workspaceContextService.getWorkspace().folders.length ? StorageScope.WORKSPACE : StorageScope.APPLICATION); + } + + this.signedOutContextKey.set(this._state.entitlement === ChatEntitlement.Unknown); + this.canSignUpContextKey.set(this._state.entitlement === ChatEntitlement.Available); + this.limitedContextKey.set(this._state.entitlement === ChatEntitlement.Limited); + this.proContextKey.set(this._state.entitlement === ChatEntitlement.Pro); + this.hiddenContext.set(!!this._state.hidden); + this.installedContext.set(!!this._state.installed); + + this._onDidChange.fire(); + } + + suspend(): void { + this.suspendedState = { ...this._state }; + this.updateBarrier = new Barrier(); + } + + resume(): void { + this.suspendedState = undefined; + this.updateBarrier?.open(); + this.updateBarrier = undefined; + } +} + +//#endregion diff --git a/src/vs/workbench/contrib/chat/common/chatEntitlementsService.ts b/src/vs/workbench/contrib/chat/common/chatEntitlementsService.ts deleted file mode 100644 index c0f6f1ce847..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatEntitlementsService.ts +++ /dev/null @@ -1,46 +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 { CancellationToken } from '../../../../base/common/cancellation.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; - -export const IChatEntitlementsService = createDecorator('chatEntitlementsService'); - -export enum ChatEntitlement { - /** Signed out */ - Unknown = 1, - /** Signed in but not yet resolved */ - Unresolved, - /** Signed in and entitled to Limited */ - Available, - /** Signed in but not entitled to Limited */ - Unavailable, - /** Signed-up to Limited */ - Limited, - /** Signed-up to Pro */ - Pro -} - -export interface IChatEntitlements { - readonly entitlement: ChatEntitlement; - readonly quotas?: IQuotas; -} - -export interface IQuotas { - readonly chatTotal?: number; - readonly completionsTotal?: number; - - readonly chatRemaining?: number; - readonly completionsRemaining?: number; - - readonly resetDate?: string; -} - -export interface IChatEntitlementsService { - - _serviceBrand: undefined; - - resolve(token: CancellationToken): Promise; -} diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index a62958b4367..9b276befbe8 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -5,6 +5,7 @@ import { asArray } from '../../../../base/common/arrays.js'; import { DeferredPromise } from '../../../../base/common/async.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; @@ -21,19 +22,19 @@ import { IRange } from '../../../../editor/common/core/range.js'; import { Location, SymbolKind, TextEdit } from '../../../../editor/common/languages.js'; import { localize } from '../../../../nls.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { MarkerSeverity } from '../../../../platform/markers/common/markers.js'; +import { IMarker, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; -import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, IChatWelcomeMessageContent, reviveSerializedAgent } from './chatAgents.js'; +import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; import { IChatRequestVariableValue } from './chatVariables.js'; +import { ChatAgentLocation } from './constants.js'; export interface IBaseChatRequestVariableEntry { id: string; fullName?: string; icon?: ThemeIcon; name: string; - isMarkedReadonly?: boolean; modelDescription?: string; range?: IOffsetRange; value: IChatRequestVariableValue; @@ -91,30 +92,66 @@ export interface ILinkVariableEntry extends Omit { readonly kind: 'image'; readonly isPasted?: boolean; + readonly isURL?: boolean; } export interface IDiagnosticVariableEntryFilterData { + readonly owner?: string; + readonly problemMessage?: string; readonly filterUri?: URI; readonly filterSeverity?: MarkerSeverity; readonly filterRange?: IRange; } export namespace IDiagnosticVariableEntryFilterData { + export const icon = Codicon.error; + + export function fromMarker(marker: IMarker): IDiagnosticVariableEntryFilterData { + return { + filterUri: marker.resource, + owner: marker.owner, + problemMessage: marker.message, + filterRange: { startLineNumber: marker.startLineNumber, endLineNumber: marker.endLineNumber, startColumn: marker.startColumn, endColumn: marker.endColumn } + }; + } + + export function toEntry(data: IDiagnosticVariableEntryFilterData): IDiagnosticVariableEntry { + return { + id: id(data), + name: label(data), + icon, + value: data, + kind: 'diagnostic' as const, + range: data.filterRange ? new OffsetRange(data.filterRange.startLineNumber, data.filterRange.endLineNumber) : undefined, + ...data, + }; + } + export function id(data: IDiagnosticVariableEntryFilterData) { - return [data.filterUri, data.filterSeverity, data.filterRange?.startLineNumber].join(':'); + return [data.filterUri, data.owner, data.filterSeverity, data.filterRange?.startLineNumber].join(':'); } export function label(data: IDiagnosticVariableEntryFilterData) { - let labelStr: string; - if (data.filterSeverity) { - const sev = data.filterRange ? MarkerSeverity.toString(data.filterSeverity) : MarkerSeverity.toStringPlural(data.filterSeverity); - labelStr = data.filterUri - ? localize('chat.attachment.problems.severity', "{0} in {1}", sev, basename(data.filterUri)) - : localize('chat.attachment.problems.severity2', "All {0}", sev); - } else { - labelStr = data.filterUri - ? localize('chat.attachment.problems.severity3', "Problems in {0}", basename(data.filterUri)) - : localize('chat.attachment.problems.severity4', "All Problems"); + const enum TrimThreshold { + MaxChars = 30, + MaxSpaceLookback = 10, + } + if (data.problemMessage) { + if (data.problemMessage.length < TrimThreshold.MaxChars) { + return data.problemMessage; + } + + // Trim the message, on a space if it would not lose too much + // data (MaxSpaceLookback) or just blindly otherwise. + const lastSpace = data.problemMessage.lastIndexOf(' ', TrimThreshold.MaxChars); + if (lastSpace === -1 || lastSpace + TrimThreshold.MaxSpaceLookback < TrimThreshold.MaxChars) { + return data.problemMessage.substring(0, TrimThreshold.MaxChars) + '…'; + } + return data.problemMessage.substring(0, lastSpace) + '…'; + } + let labelStr = localize('chat.attachment.problems.all', "All Problems"); + if (data.filterUri) { + labelStr = localize('chat.attachment.problems.inFile', "Problems in {0}", basename(data.filterUri)); } return labelStr; @@ -911,7 +948,6 @@ export interface IChatModel { readonly initState: ChatModelInitState; readonly initialLocation: ChatAgentLocation; readonly title: string; - readonly welcomeMessage: IChatWelcomeMessageContent | undefined; readonly sampleQuestions: IChatFollowup[] | undefined; readonly requestInProgress: boolean; readonly requestPausibility: ChatPauseState; @@ -1078,6 +1114,7 @@ export type IChatChangeEvent = | IChatSetAgentEvent | IChatMoveEvent | IChatSetHiddenEvent + | IChatCompletedRequestEvent ; export interface IChatAddRequestEvent { @@ -1090,6 +1127,11 @@ export interface IChatChangedRequestEvent { request: IChatRequestModel; } +export interface IChatCompletedRequestEvent { + kind: 'completedRequest'; + request: IChatRequestModel; +} + export interface IChatAddResponseEvent { kind: 'addResponse'; response: IChatResponseModel; @@ -1165,11 +1207,6 @@ export class ChatModel extends Disposable implements IChatModel { private _initState: ChatModelInitState = ChatModelInitState.Created; private _isInitializedDeferred = new DeferredPromise(); - private _welcomeMessage: IChatWelcomeMessageContent | undefined; - get welcomeMessage(): IChatWelcomeMessageContent | undefined { - return this._welcomeMessage; - } - private _sampleQuestions: IChatFollowup[] | undefined; get sampleQuestions(): IChatFollowup[] | undefined { return this._sampleQuestions; @@ -1391,14 +1428,13 @@ export class ChatModel extends Disposable implements IChatModel { this._isInitializedDeferred = new DeferredPromise(); } - initialize(welcomeMessage?: IChatWelcomeMessageContent, sampleQuestions?: IChatFollowup[]): void { + initialize(sampleQuestions?: IChatFollowup[]): void { if (this.initState !== ChatModelInitState.Initializing) { // Must call startInitialize before initialize, and only call it once throw new Error(`ChatModel is in the wrong state for initialize: ${ChatModelInitState[this.initState]}`); } this._initState = ChatModelInitState.Initialized; - this._welcomeMessage = welcomeMessage; this._sampleQuestions = sampleQuestions; this._isInitializedDeferred.complete(); @@ -1549,6 +1585,7 @@ export class ChatModel extends Disposable implements IChatModel { } request.response.complete(); + this._onDidChange.fire({ kind: 'completedRequest', request }); } setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void { diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index 0dff77c6e12..2542fa4ca67 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -7,9 +7,11 @@ import { revive } from '../../../../base/common/marshalling.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/offsetRange.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; +import { IChatAgentCommand, IChatAgentData, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; +import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatModel.js'; import { IChatSlashData } from './chatSlashCommands.js'; -import { IChatRequestVariableValue } from './chatVariables.js'; +import { IChatRequestProblemsVariable, IChatRequestVariableValue } from './chatVariables.js'; +import { ChatAgentLocation } from './constants.js'; import { IToolData } from './languageModelToolsService.js'; // These are in a separate file to avoid circular dependencies with the dependencies of the parser @@ -84,6 +86,10 @@ export class ChatRequestToolPart implements IParsedChatRequestPart { get promptText(): string { return this.text; } + + toVariableEntry(): IChatRequestVariableEntry { + return { id: this.toolId, name: this.toolName, range: this.range, value: undefined, isTool: true, icon: ThemeIcon.isThemeIcon(this.icon) ? this.icon : undefined, fullName: this.displayName }; + } } /** @@ -143,7 +149,7 @@ export class ChatRequestSlashCommandPart implements IParsedChatRequestPart { export class ChatRequestDynamicVariablePart implements IParsedChatRequestPart { static readonly Kind = 'dynamic'; readonly kind = ChatRequestDynamicVariablePart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string, readonly id: string, readonly modelDescription: string | undefined, readonly data: IChatRequestVariableValue, readonly fullName?: string, readonly icon?: ThemeIcon, readonly isFile?: boolean) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string, readonly id: string, readonly modelDescription: string | undefined, readonly data: IChatRequestVariableValue, readonly fullName?: string, readonly icon?: ThemeIcon, readonly isFile?: boolean, readonly isDirectory?: boolean) { } get referenceText(): string { return this.text.replace(chatVariableLeader, ''); @@ -152,6 +158,14 @@ export class ChatRequestDynamicVariablePart implements IParsedChatRequestPart { get promptText(): string { return this.text; } + + toVariableEntry(): IChatRequestVariableEntry { + if (this.id === 'vscode.problems') { + return IDiagnosticVariableEntryFilterData.toEntry((this.data as IChatRequestProblemsVariable).filter); + } + + return { id: this.id, name: this.referenceText, range: this.range, value: this.data, fullName: this.fullName, icon: this.icon, isFile: this.isFile, isDirectory: this.isDirectory }; + } } export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsedChatRequest { @@ -212,7 +226,8 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed revive((part as ChatRequestDynamicVariablePart).data), (part as ChatRequestDynamicVariablePart).fullName, (part as ChatRequestDynamicVariablePart).icon, - (part as ChatRequestDynamicVariablePart).isFile + (part as ChatRequestDynamicVariablePart).isFile, + (part as ChatRequestDynamicVariablePart).isDirectory ); } else { throw new Error(`Unknown chat request part: ${part.kind}`); diff --git a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts index 401211b3c4f..92549dc2480 100644 --- a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { RawChatParticipantLocation } from './constants.js'; + export interface IRawChatCommandContribution { name: string; description: string; @@ -13,8 +15,6 @@ export interface IRawChatCommandContribution { disambiguation?: { category: string; categoryName?: string /** Deprecated */; description: string; examples: string[] }[]; } -export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook' | 'editing-session'; - export interface IRawChatParticipantContribution { id: string; name: string; diff --git a/src/vs/workbench/contrib/chat/common/chatQuotasService.ts b/src/vs/workbench/contrib/chat/common/chatQuotasService.ts deleted file mode 100644 index e79bcf34a58..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatQuotasService.ts +++ /dev/null @@ -1,136 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ChatContextKeys } from './chatContextKeys.js'; - -export const IChatQuotasService = createDecorator('chatQuotasService'); - -export interface IChatQuotasService { - - _serviceBrand: undefined; - - readonly onDidChangeQuotaExceeded: Event; - readonly onDidChangeQuotaRemaining: Event; - - readonly quotas: IChatQuotas; - - acceptQuotas(quotas: IChatQuotas): void; - clearQuotas(): void; -} - -export interface IChatQuotas { - chatQuotaExceeded: boolean; - completionsQuotaExceeded: boolean; - quotaResetDate: Date | undefined; - - chatTotal?: number; - completionsTotal?: number; - - chatRemaining?: number; - completionsRemaining?: number; -} - -export class ChatQuotasService extends Disposable implements IChatQuotasService { - - declare _serviceBrand: undefined; - - private readonly _onDidChangeQuotaExceeded = this._register(new Emitter()); - readonly onDidChangeQuotaExceeded = this._onDidChangeQuotaExceeded.event; - - private readonly _onDidChangeQuotaRemaining = this._register(new Emitter()); - readonly onDidChangeQuotaRemaining = this._onDidChangeQuotaRemaining.event; - - private _quotas: IChatQuotas = { chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: undefined }; - get quotas(): IChatQuotas { return this._quotas; } - - private readonly chatQuotaExceededContextKey = ChatContextKeys.chatQuotaExceeded.bindTo(this.contextKeyService); - private readonly completionsQuotaExceededContextKey = ChatContextKeys.completionsQuotaExceeded.bindTo(this.contextKeyService); - - private ExtensionQuotaContextKeys = { - chatQuotaExceeded: 'github.copilot.chat.quotaExceeded', - completionsQuotaExceeded: 'github.copilot.completions.quotaExceeded', - }; - - constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService, - ) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - const chatQuotaExceededSet = new Set([this.ExtensionQuotaContextKeys.chatQuotaExceeded]); - const completionsQuotaExceededSet = new Set([this.ExtensionQuotaContextKeys.completionsQuotaExceeded]); - - this._register(this.contextKeyService.onDidChangeContext(e => { - let changed = false; - if (e.affectsSome(chatQuotaExceededSet)) { - const newChatQuotaExceeded = this.contextKeyService.getContextKeyValue(this.ExtensionQuotaContextKeys.chatQuotaExceeded); - if (typeof newChatQuotaExceeded === 'boolean' && newChatQuotaExceeded !== this._quotas.chatQuotaExceeded) { - this._quotas.chatQuotaExceeded = newChatQuotaExceeded; - changed = true; - } - } - - if (e.affectsSome(completionsQuotaExceededSet)) { - const newCompletionsQuotaExceeded = this.contextKeyService.getContextKeyValue(this.ExtensionQuotaContextKeys.completionsQuotaExceeded); - if (typeof newCompletionsQuotaExceeded === 'boolean' && newCompletionsQuotaExceeded !== this._quotas.completionsQuotaExceeded) { - this._quotas.completionsQuotaExceeded = newCompletionsQuotaExceeded; - changed = true; - } - } - - if (changed) { - this.updateContextKeys(); - this._onDidChangeQuotaExceeded.fire(); - } - })); - } - - acceptQuotas(quotas: IChatQuotas): void { - const oldQuota = this._quotas; - this._quotas = this.massageQuotas(quotas); - this.updateContextKeys(); - - if ( - oldQuota.chatQuotaExceeded !== this._quotas.chatQuotaExceeded || - oldQuota.completionsQuotaExceeded !== this._quotas.completionsQuotaExceeded - ) { - this._onDidChangeQuotaExceeded.fire(); - } - - if ( - oldQuota.chatRemaining !== this._quotas.chatRemaining || - oldQuota.completionsRemaining !== this._quotas.completionsRemaining - ) { - this._onDidChangeQuotaRemaining.fire(); - } - } - - private massageQuotas(quotas: IChatQuotas): IChatQuotas { - return { - ...quotas, - chatTotal: typeof quotas.chatTotal === 'number' ? Math.floor(quotas.chatTotal / 10) : undefined, - chatRemaining: typeof quotas.chatRemaining === 'number' ? Math.floor(Math.max(0, quotas.chatRemaining) / 10) : undefined, - completionsRemaining: typeof quotas.completionsRemaining === 'number' ? Math.max(0, quotas.completionsRemaining) : undefined - }; - } - - clearQuotas(): void { - if (this.quotas.chatQuotaExceeded || this.quotas.completionsQuotaExceeded) { - this.acceptQuotas({ chatQuotaExceeded: false, completionsQuotaExceeded: false, quotaResetDate: undefined }); - } - } - - private updateContextKeys(): void { - this.chatQuotaExceededContextKey.set(this._quotas.chatQuotaExceeded); - this.completionsQuotaExceededContextKey.set(this._quotas.completionsQuotaExceeded); - } -} diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index b710d18bd31..251e1fb291d 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -6,10 +6,11 @@ import { OffsetRange } from '../../../../editor/common/core/offsetRange.js'; import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { Range } from '../../../../editor/common/core/range.js'; -import { ChatAgentLocation, IChatAgentData, IChatAgentService } from './chatAgents.js'; +import { IChatAgentData, IChatAgentService } from './chatAgents.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestToolPart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from './chatParserTypes.js'; import { IChatSlashCommandService } from './chatSlashCommands.js'; import { IChatVariablesService, IDynamicVariable } from './chatVariables.js'; +import { ChatAgentLocation, ChatMode } from './constants.js'; import { ILanguageModelToolsService } from './languageModelToolsService.js'; const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent @@ -19,6 +20,7 @@ const slashReg = /\/([\w_\-]+)(?=(\s|$|\b))/i; // A / command export interface IChatParserContext { /** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */ selectedAgent?: IChatAgentData; + mode?: ChatMode; } export class ChatRequestParser { @@ -45,7 +47,7 @@ export class ChatRequestParser { } else if (char === chatAgentLeader) { newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context); } else if (char === chatSubcommandLeader) { - newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts, location); + newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context); } if (!newPart) { @@ -94,7 +96,7 @@ export class ChatRequestParser { private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: Array, location: ChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | undefined { const nextAgentMatch = message.match(agentReg); - if (!nextAgentMatch) { + if (!nextAgentMatch || context?.mode !== undefined && context.mode !== ChatMode.Chat) { return; } @@ -157,7 +159,7 @@ export class ChatRequestParser { return; } - private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { + private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: ChatAgentLocation, context?: IChatParserContext): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { const nextSlashMatch = remainingMessage.match(slashReg); if (!nextSlashMatch) { return; @@ -199,7 +201,7 @@ export class ChatRequestParser { return new ChatRequestSlashCommandPart(slashRange, slashEditorRange, slashCommand); } else { // check for with default agent for this location - const defaultAgent = this.agentService.getDefaultAgent(location); + const defaultAgent = this.agentService.getDefaultAgent(location, context?.mode); const subCommand = defaultAgent?.slashCommands.find(c => c.name === command); if (subCommand) { // Valid default agent subcommand @@ -219,7 +221,7 @@ export class ChatRequestParser { const length = refAtThisPosition.range.endColumn - refAtThisPosition.range.startColumn; const text = message.substring(0, length); const range = new OffsetRange(offset, offset + length); - return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.id, refAtThisPosition.modelDescription, refAtThisPosition.data, refAtThisPosition.fullName, refAtThisPosition.icon, refAtThisPosition.isFile); + return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.id, refAtThisPosition.modelDescription, refAtThisPosition.data, refAtThisPosition.fullName, refAtThisPosition.icon, refAtThisPosition.isFile, refAtThisPosition.isDirectory); } return; diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 9a6a32cb43e..de11ce552d0 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -16,11 +16,12 @@ import { FileType } from '../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { IWorkspaceSymbol } from '../../search/common/search.js'; -import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult } from './chatAgents.js'; +import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from './chatAgents.js'; import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData } from './chatModel.js'; import { IParsedChatRequest } from './chatParserTypes.js'; import { IChatParserContext } from './chatRequestParser.js'; import { IChatRequestVariableValue } from './chatVariables.js'; +import { ChatAgentLocation, ChatMode } from './constants.js'; import { IPreparedToolInvocation, IToolConfirmationMessages, IToolResult } from './languageModelToolsService.js'; export interface IChatRequest { @@ -28,12 +29,19 @@ export interface IChatRequest { variables: Record; } +export enum ChatErrorLevel { + Info = 0, + Warning = 1, + Error = 2 +} + export interface IChatResponseErrorDetails { message: string; responseIsIncomplete?: boolean; responseIsFiltered?: boolean; responseIsRedacted?: boolean; isQuotaExceeded?: boolean; + level?: ChatErrorLevel; } export interface IChatResponseProgressFileTreeData { @@ -404,7 +412,7 @@ export interface IChatTransferredSessionData { sessionId: string; inputValue: string; location: ChatAgentLocation; - toolsAgentModeEnabled: boolean; + mode: ChatMode; } export interface IChatSendRequestResponseState { @@ -437,7 +445,9 @@ export interface IChatTerminalLocationData { export type IChatLocationData = IChatEditorLocationData | IChatNotebookLocationData | IChatTerminalLocationData; export interface IChatSendRequestOptions { + mode?: ChatMode; userSelectedModelId?: string; + userSelectedTools?: string[]; location?: ChatAgentLocation; locationData?: IChatLocationData; parserContext?: IChatParserContext; @@ -498,6 +508,9 @@ export interface IChatService { onDidDisposeSession: Event<{ sessionId: string; reason: 'initializationFailed' | 'cleared' }>; transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void; + + readonly unifiedViewEnabled: boolean; + isEditingLocation(location: ChatAgentLocation): boolean; } export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActivation'; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index f519d27d932..42f2d1d6386 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -5,6 +5,7 @@ import { DeferredPromise } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { memoize } from '../../../../base/common/decorators.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { ErrorNoTelemetry } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; @@ -25,7 +26,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from './chatAgents.js'; +import { IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from './chatAgents.js'; import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from './chatParserTypes.js'; import { ChatRequestParser } from './chatRequestParser.js'; @@ -33,6 +34,7 @@ import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChat import { ChatServiceTelemetry } from './chatServiceTelemetry.js'; import { IChatSlashCommandService } from './chatSlashCommands.js'; import { IChatVariablesService } from './chatVariables.js'; +import { ChatAgentLocation, ChatConfiguration, ChatMode } from './constants.js'; import { ChatMessageRole, IChatMessage } from './languageModels.js'; import { ILanguageModelToolsService } from './languageModelToolsService.js'; @@ -45,7 +47,7 @@ interface IChatTransfer { chat: ISerializableChatData; inputValue: string; location: ChatAgentLocation; - toolsAgentModeEnabled: boolean; + mode: ChatMode; } const SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS = 1000 * 60; @@ -134,6 +136,11 @@ export class ChatService extends Disposable implements IChatService { private readonly _sessionFollowupCancelTokens = this._register(new DisposableMap()); private readonly _chatServiceTelemetry: ChatServiceTelemetry; + @memoize + public get unifiedViewEnabled(): boolean { + return !!this.configurationService.getValue(ChatConfiguration.UnifiedChatView); + } + constructor( @IStorageService private readonly storageService: IStorageService, @ILogService private readonly logService: ILogService, @@ -171,7 +178,7 @@ export class ChatService extends Disposable implements IChatService { sessionId: transferredChat.sessionId, inputValue: transferredData.inputValue, location: transferredData.location, - toolsAgentModeEnabled: transferredData.toolsAgentModeEnabled, + mode: transferredData.mode, }; } @@ -197,7 +204,26 @@ export class ChatService extends Disposable implements IChatService { .filter(session => !this._sessionModels.has(session.sessionId)) .filter(session => session.requests.length)); allSessions.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0)); + + // Only keep one persisted edit session, the latest one. This would be the current one if it's live. + // No way to know whether it's currently live or if it has been cleared and there is no current session. + // But ensure that we don't store multiple edit sessions. + let hasPersistedEditSession = false; + allSessions = allSessions.filter(s => { + if (s.initialLocation === ChatAgentLocation.EditingSession) { + if (hasPersistedEditSession) { + return false; + } else { + hasPersistedEditSession = true; + return true; + } + } + + return true; + }); + allSessions = allSessions.slice(0, maxPersistedSessions); + if (allSessions.length) { this.trace('onWillSaveState', `Persisting ${allSessions.length} sessions`); } @@ -423,9 +449,8 @@ export class ChatService extends Disposable implements IChatService { throw new ErrorNoTelemetry('No default agent registered'); } - const welcomeMessage = await defaultAgent.provideWelcomeMessage?.(token) ?? undefined; const sampleQuestions = await defaultAgent.provideSampleQuestions?.(model.initialLocation, token) ?? undefined; - model.initialize(welcomeMessage, sampleQuestions); + model.initialize(sampleQuestions); } catch (err) { this.trace('startSession', `initializeSession failed: ${err}`); model.setInitializationError(err); @@ -454,7 +479,8 @@ export class ChatService extends Disposable implements IChatService { const isTransferred = this.transferredSessionData?.sessionId === sessionId; if (isTransferred) { - this.chatAgentService.toggleToolsAgentMode(this.transferredSessionData.toolsAgentModeEnabled); + // TODO + // this.chatAgentService.toggleToolsAgentMode(this.transferredSessionData.toolsAgentModeEnabled); this._transferredSessionData = undefined; } @@ -482,7 +508,7 @@ export class ChatService extends Disposable implements IChatService { const location = options?.location ?? model.initialLocation; const attempt = options?.attempt ?? 0; const enableCommandDetection = !options?.noCommandDetection; - const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; + const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.mode)!; model.removeRequest(request.id, ChatRequestRemovalReason.Resend); @@ -535,7 +561,7 @@ export class ChatService extends Disposable implements IChatService { const location = options?.location ?? model.initialLocation; const attempt = options?.attempt ?? 0; - const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; + const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.mode)!; const parsedRequest = this.parseChatRequest(sessionId, request, location, options); const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent; @@ -556,7 +582,7 @@ export class ChatService extends Disposable implements IChatService { if (!agent) { throw new Error(`Unknown agent: ${options.agentId}`); } - parserContext = { selectedAgent: agent }; + parserContext = { selectedAgent: agent, mode: options.mode }; const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : ''; request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`; } @@ -678,11 +704,12 @@ export class ChatService extends Disposable implements IChatService { locationData: request.locationData, acceptedConfirmationData: options?.acceptedConfirmationData, rejectedConfirmationData: options?.rejectedConfirmationData, - userSelectedModelId: options?.userSelectedModelId + userSelectedModelId: options?.userSelectedModelId, + userSelectedTools: options?.userSelectedTools } satisfies IChatAgentRequest; }; - if (this.configurationService.getValue('chat.detectParticipant.enabled') !== false && this.chatAgentService.hasChatParticipantDetectionProviders() && !agentPart && !commandPart && !agentSlashCommandPart && enableCommandDetection) { + if (this.configurationService.getValue('chat.detectParticipant.enabled') !== false && this.chatAgentService.hasChatParticipantDetectionProviders() && !agentPart && !commandPart && !agentSlashCommandPart && enableCommandDetection && options?.mode !== ChatMode.Agent && options?.mode !== ChatMode.Edit) { // We have no agent or command to scope history with, pass the full history to the participant detection provider const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, model.sessionId, location, defaultAgent.id); @@ -872,13 +899,13 @@ export class ChatService extends Disposable implements IChatService { private getHistoryEntriesFromModel(requests: IChatRequestModel[], sessionId: string, location: ChatAgentLocation, forAgentId: string): IChatAgentHistoryEntry[] { const history: IChatAgentHistoryEntry[] = []; + const agent = this.chatAgentService.getAgent(forAgentId); for (const request of requests) { if (!request.response) { continue; } - const defaultAgentId = this.chatAgentService.getDefaultAgent(location)?.id; - if (forAgentId !== request.response.agent?.id && forAgentId !== defaultAgentId) { + if (forAgentId !== request.response.agent?.id && !agent?.isDefault) { // An agent only gets to see requests that were sent to this agent. // The default agent (the undefined case) gets to see all of them. continue; @@ -1012,12 +1039,16 @@ export class ChatService extends Disposable implements IChatService { toWorkspace: toWorkspace, inputValue: transferredSessionData.inputValue, location: transferredSessionData.location, - toolsAgentModeEnabled: transferredSessionData.toolsAgentModeEnabled, + mode: transferredSessionData.mode, }); this.storageService.store(globalChatKey, JSON.stringify(existingRaw), StorageScope.PROFILE, StorageTarget.MACHINE); this.trace('transferChatSession', `Transferred session ${model.sessionId} to workspace ${toWorkspace.toString()}`); } + + isEditingLocation(location: ChatAgentLocation): boolean { + return location === ChatAgentLocation.EditingSession || this.unifiedViewEnabled; + } } function getCodeBlocks(text: string): string[] { diff --git a/src/vs/workbench/contrib/chat/common/chatTransferService.ts b/src/vs/workbench/contrib/chat/common/chatTransferService.ts new file mode 100644 index 00000000000..22a2eb31aa8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatTransferService.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; +import { isChatTransferredWorkspace, areWorkspaceFoldersEmpty } from '../../../services/workspaces/common/workspaceUtils.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +export const IChatTransferService = createDecorator('chatTransferService'); + +export interface IChatTransferService { + readonly _serviceBrand: undefined; + + checkAndSetWorkspaceTrust(): Promise; +} + +export class ChatTransferService implements IChatTransferService { + _serviceBrand: undefined; + + constructor( + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IStorageService private readonly storageService: IStorageService, + @IFileService private readonly fileService: IFileService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService + ) { } + + async checkAndSetWorkspaceTrust(): Promise { + const workspace = this.workspaceService.getWorkspace(); + if (isChatTransferredWorkspace(workspace, this.storageService) && await areWorkspaceFoldersEmpty(workspace, this.fileService)) { + await this.workspaceTrustManagementService.setWorkspaceTrust(true); + } + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index de02d68cf91..809d5637c53 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -9,10 +9,10 @@ import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { Location } from '../../../../editor/common/languages.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ChatAgentLocation } from './chatAgents.js'; -import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from './chatModel.js'; +import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatModel.js'; import { IParsedChatRequest } from './chatParserTypes.js'; import { IChatContentReference, IChatProgressMessage } from './chatService.js'; +import { ChatAgentLocation } from './constants.js'; export interface IChatVariableData { id: string; @@ -24,7 +24,15 @@ export interface IChatVariableData { canTakeArgument?: boolean; } -export type IChatRequestVariableValue = string | URI | Location | unknown | Uint8Array; +export interface IChatRequestProblemsVariable { + id: 'vscode.problems'; + filter: IDiagnosticVariableEntryFilterData; +} + +export const isIChatRequestProblemsVariable = (obj: unknown): obj is IChatRequestProblemsVariable => + typeof obj === 'object' && obj !== null && 'id' in obj && (obj as IChatRequestProblemsVariable).id === 'vscode.problems'; + +export type IChatRequestVariableValue = string | URI | Location | unknown | Uint8Array | IChatRequestProblemsVariable; export type IChatVariableResolverProgress = | IChatContentReference @@ -55,5 +63,6 @@ export interface IDynamicVariable { prefix?: string; modelDescription?: string; isFile?: boolean; + isDirectory?: boolean; data: IChatRequestVariableValue; } diff --git a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts index d2ab31d58ce..4522c0b7bd3 100644 --- a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts +++ b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts @@ -8,10 +8,10 @@ import { URI } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { Memento } from '../../../common/memento.js'; -import { ChatAgentLocation } from './chatAgents.js'; import { WorkingSetEntryState } from './chatEditingService.js'; import { IChatRequestVariableEntry } from './chatModel.js'; import { CHAT_PROVIDER_ID } from './chatParticipantContribTypes.js'; +import { ChatAgentLocation, ChatMode } from './constants.js'; export interface IChatHistoryEntry { text: string; @@ -23,6 +23,7 @@ export interface IChatInputState { [key: string]: any; chatContextAttachments?: ReadonlyArray; chatWorkingSet?: ReadonlyArray<{ uri: URI; state: WorkingSetEntryState }>; + chatMode?: ChatMode; } export const IChatWidgetHistoryService = createDecorator('IChatWidgetHistoryService'); diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts new file mode 100644 index 00000000000..9c42f7bc304 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export enum ChatConfiguration { + UnifiedChatView = 'chat.experimental.unifiedChatView', +} + +export enum ChatMode { + Chat = 'chat', + Edit = 'edit', + Agent = 'agent' +} + +export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook' | 'editing-session'; + +export enum ChatAgentLocation { + Panel = 'panel', + Terminal = 'terminal', + Notebook = 'notebook', + Editor = 'editor', + EditingSession = 'editing-session', +} + +export namespace ChatAgentLocation { + export function fromRaw(value: RawChatParticipantLocation | string): ChatAgentLocation { + switch (value) { + case 'panel': return ChatAgentLocation.Panel; + case 'terminal': return ChatAgentLocation.Terminal; + case 'notebook': return ChatAgentLocation.Notebook; + case 'editor': return ChatAgentLocation.Editor; + case 'editing-session': return ChatAgentLocation.EditingSession; + } + return ChatAgentLocation.Panel; + } +} diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index 8a692ee4cf0..4e873111014 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -37,6 +37,7 @@ export interface IToolInvocation { tokenBudget?: number; context: IToolInvocationContext | undefined; chatRequestId?: string; + chatInteractionId?: string; toolSpecificData?: IChatTerminalToolInvocationData; } diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 8424946dac5..3db81d2941c 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -89,6 +89,7 @@ export interface ILanguageModelChatMetadata { readonly capabilities?: { readonly vision?: boolean; readonly toolCalling?: boolean; + readonly agentMode?: boolean; }; } diff --git a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts index 84e7d62551b..e19588df4e6 100644 --- a/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts +++ b/src/vs/workbench/contrib/chat/common/promptFileReferenceErrors.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../base/common/uri.js'; +import { basename } from '../../../../base/common/path.js'; +import { assert, assertNever } from '../../../../base/common/assert.js'; /** * Base prompt parsing error class. */ -export abstract class ParseError extends Error { +abstract class ParseError extends Error { /** * Error type name. */ @@ -40,22 +42,6 @@ export abstract class ParseError extends Error { } } -/** - * A generic error for failing to resolve prompt contents stream. - */ -export class FailedToResolveContentsStream extends ParseError { - public override errorType = 'FailedToResolveContentsStream'; - - constructor( - public readonly uri: URI, - public readonly originalError: unknown, - message: string = `Failed to resolve prompt contents stream for '${uri.toString()}': ${originalError}.`, - ) { - super(message); - } -} - - /** * Base resolve error class used when file reference resolution fails. */ @@ -71,6 +57,22 @@ export abstract class ResolveError extends ParseError { } } +/** + * A generic error for failing to resolve prompt contents stream. + */ +export class FailedToResolveContentsStream extends ResolveError { + public override errorType = 'FailedToResolveContentsStream'; + + constructor( + uri: URI, + public readonly originalError: unknown, + message: string = `Failed to resolve prompt contents stream for '${uri.toString()}': ${originalError}.`, + ) { + super(uri, message); + } +} + + /** * Error that reflects the case when attempt to open target file fails. */ @@ -89,6 +91,12 @@ export class OpenFailed extends FailedToResolveContentsStream { } } +/** + * Character use to join filenames/paths in a chain of references that + * lead to recursion. + */ +const DEFAULT_RECURSIVE_PATH_JOIN_CHAR = ' -> '; + /** * Error that reflects the case when attempt resolve nested file * references failes due to a recursive reference, e.g., @@ -106,23 +114,67 @@ export class OpenFailed extends FailedToResolveContentsStream { export class RecursiveReference extends ResolveError { public override errorType = 'RecursiveReferenceError'; + /** + * Cached default string representation of the recursive path. + */ + private defaultPathStringCache: string | undefined; + constructor( uri: URI, public readonly recursivePath: string[], ) { - const references = recursivePath.join(' -> '); + // sanity check - a recursive path must always have at least + // two items in the list, otherwise it is not a recursive loop + assert( + recursivePath.length >= 2, + `Recursive path must contain at least two paths, got '${recursivePath.length}'.`, + ); super( - uri, - `Recursive references found: ${references}.`, + uri, 'Recursive references found.', ); } + public override get message(): string { + return `${super.message} ${this.getRecursivePathString('fullpath')}`; + } + /** * Returns a string representation of the recursive path. */ - public get recursivePathString(): string { - return this.recursivePath.join(' -> '); + public getRecursivePathString( + filename: 'basename' | 'fullpath', + pathJoinCharacter: string = DEFAULT_RECURSIVE_PATH_JOIN_CHAR, + ): string { + const isDefault = (filename === 'fullpath') && + (pathJoinCharacter === DEFAULT_RECURSIVE_PATH_JOIN_CHAR); + + if (isDefault && (this.defaultPathStringCache !== undefined)) { + return this.defaultPathStringCache; + } + + const result = this.recursivePath + .map((path) => { + if (filename === 'fullpath') { + return `'${path}'`; + } + + if (filename === 'basename') { + return `'${basename(path)}'`; + } + + assertNever( + filename, + `Unknown filename format '${filename}'.`, + ); + }) + .join(pathJoinCharacter); + + if (isDefault) { + this.defaultPathStringCache = result; + } + + return result; } /** @@ -138,7 +190,22 @@ export class RecursiveReference extends ResolveError { return false; } - return this.recursivePathString === other.recursivePathString; + // performance optimization - compare number of paths in the + // recursive path chains first to avoid comparison of all strings + if (this.recursivePath.length !== other.recursivePath.length) { + return false; + } + + const myRecursivePath = this.getRecursivePathString('fullpath'); + const theirRecursivePath = other.getRecursivePathString('fullpath'); + + // performance optimization - if the path lengths don't match, + // no need to compare entire strings as they must be different + if (myRecursivePath.length !== theirRecursivePath.length) { + return false; + } + + return myRecursivePath === theirRecursivePath; } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts index 06d8e1620ec..b1bd3de2b92 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts @@ -3,166 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { FileReference } from './tokens/fileReference.js'; +import { PromptToken } from './tokens/promptToken.js'; import { VSBuffer } from '../../../../../../base/common/buffer.js'; -import { Range } from '../../../../../../editor/common/core/range.js'; +import { assertNever } from '../../../../../../base/common/assert.js'; import { ReadableStream } from '../../../../../../base/common/stream.js'; import { BaseDecoder } from '../../../../../../base/common/codecs/baseDecoder.js'; -import { Tab } from '../../../../../../editor/common/codecs/simpleCodec/tokens/tab.js'; -import { Word } from '../../../../../../editor/common/codecs/simpleCodec/tokens/word.js'; +import { PromptVariable, PromptVariableWithData } from './tokens/promptVariable.js'; import { Hash } from '../../../../../../editor/common/codecs/simpleCodec/tokens/hash.js'; -import { Space } from '../../../../../../editor/common/codecs/simpleCodec/tokens/space.js'; -import { Colon } from '../../../../../../editor/common/codecs/simpleCodec/tokens/colon.js'; -import { NewLine } from '../../../../../../editor/common/codecs/linesCodec/tokens/newLine.js'; -import { FormFeed } from '../../../../../../editor/common/codecs/simpleCodec/tokens/formFeed.js'; -import { VerticalTab } from '../../../../../../editor/common/codecs/simpleCodec/tokens/verticalTab.js'; import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; -import { CarriageReturn } from '../../../../../../editor/common/codecs/linesCodec/tokens/carriageReturn.js'; -import { ParserBase, TAcceptTokenResult } from '../../../../../../editor/common/codecs/simpleCodec/parserBase.js'; +import { PartialPromptVariableName, PartialPromptVariableWithData } from './parsers/promptVariableParser.js'; import { MarkdownDecoder, TMarkdownToken } from '../../../../../../editor/common/codecs/markdownCodec/markdownDecoder.js'; /** * Tokens produced by this decoder. */ -export type TChatPromptToken = MarkdownLink | FileReference; - -/** - * The Parser responsible for processing a `prompt variable name` syntax from - * a sequence of tokens (e.g., `#variable:`). - * - * The parsing process starts with single `#` token, then can accept `file` word, - * followed by the `:` token, resulting in the tokens sequence equivalent to - * the `#file:` text sequence. In this successful case, the parser transitions into - * the {@linkcode PartialPromptFileReference} parser to continue the parsing process. - */ -class PartialPromptVariableName extends ParserBase { - constructor(token: Hash) { - super([token]); - } - - public accept(token: TMarkdownToken): TAcceptTokenResult { - // given we currently hold the `#` token, if we receive a `file` word, - // we can successfully proceed to the next token in the sequence - if (token instanceof Word) { - if (token.text === 'file') { - this.currentTokens.push(token); - - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } - - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - // if we receive the `:` token, we can successfully proceed to the next - // token in the sequence `only if` the previous token was a `file` word - // therefore for currently tokens sequence equivalent to the `#file` text - if (token instanceof Colon) { - const lastToken = this.currentTokens[this.currentTokens.length - 1]; - - if (lastToken instanceof Word) { - this.currentTokens.push(token); - - return { - result: 'success', - nextParser: new PartialPromptFileReference(this.currentTokens), - wasTokenConsumed: true, - }; - } - } - - // all other cases are failures and we don't consume the offending token - return { - result: 'failure', - wasTokenConsumed: false, - }; - } -} - -/** - * List of characters that stop a prompt variable sequence. - */ -const PROMPT_FILE_REFERENCE_STOP_CHARACTERS: readonly string[] = [Space, Tab, CarriageReturn, NewLine, VerticalTab, FormFeed] - .map((token) => { return token.symbol; }); - -/** - * Parser responsible for processing the `file reference` syntax part from - * a sequence of tokens (e.g., #variable:`./some/file/path.md`). - * - * The parsing process starts with the sequence of `#`, `file`, and `:` tokens, - * then can accept a sequence of tokens until one of the tokens defined in - * the {@linkcode PROMPT_FILE_REFERENCE_STOP_CHARACTERS} list is encountered. - * This sequence of tokens is treated as a `file path` part of the `#file:` variable, - * and in the successful case, the parser transitions into the {@linkcode FileReference} - * token which signifies the end of the file reference text parsing process. - */ -class PartialPromptFileReference extends ParserBase { - /** - * Set of tokens that were accumulated so far. - */ - private readonly fileReferenceTokens: (Hash | Word | Colon)[]; - - constructor(tokens: (Hash | Word | Colon)[]) { - super([]); - - this.fileReferenceTokens = tokens; - } - - /** - * List of tokens that were accumulated so far. - */ - public override get tokens(): readonly (Hash | Word | Colon)[] { - return [...this.fileReferenceTokens, ...this.currentTokens]; - } - - /** - * Return the `FileReference` instance created from the current object. - */ - public asFileReference(): FileReference { - // use only tokens in the `currentTokens` list to - // create the path component of the file reference - const path = this.currentTokens - .map((token) => { return token.text; }) - .join(''); - - const firstToken = this.tokens[0]; - - const range = new Range( - firstToken.range.startLineNumber, - firstToken.range.startColumn, - firstToken.range.startLineNumber, - firstToken.range.startColumn + FileReference.TOKEN_START.length + path.length, - ); - - return new FileReference(range, path); - } - - public accept(token: TMarkdownToken): TAcceptTokenResult { - // any of stop characters is are breaking a prompt variable sequence - if (PROMPT_FILE_REFERENCE_STOP_CHARACTERS.includes(token.text)) { - return { - result: 'success', - wasTokenConsumed: false, - nextParser: this.asFileReference(), - }; - } - - // any other token can be included in the sequence so accumulate - // it and continue with using the current parser instance - this.currentTokens.push(token); - return { - result: 'success', - wasTokenConsumed: true, - nextParser: this, - }; - } -} +export type TChatPromptToken = MarkdownLink | PromptVariable | PromptVariableWithData; /** * Decoder for the common chatbot prompt message syntax. @@ -170,11 +25,11 @@ class PartialPromptFileReference extends ParserBase { /** - * Currently active parser object that is used to parse a well-known equence of - * tokens, for instance, a `file reference` that consists of `hash`, `word`, and - * `colon` tokens sequence plus following file path part. + * Currently active parser object that is used to parse a well-known sequence of + * tokens, for instance, a `#file:/path/to/file.md` link that consists of `hash`, + * `word`, and `colon` tokens sequence plus the `file path` part that follows. */ - private current?: PartialPromptVariableName; + private current?: PartialPromptVariableName | PartialPromptVariableWithData; constructor( stream: ReadableStream, @@ -185,7 +40,7 @@ export class ChatPromptDecoder extends BaseDecoder { return token.symbol; }); + +/** + * List of characters that cannot be in a variable name (excluding the {@link STOP_CHARACTERS}). + */ +export const INVALID_NAME_CHARACTERS: readonly string[] = [Hash, Colon, ExclamationMark, LeftAngleBracket, RightAngleBracket, LeftBracket, RightBracket] + .map((token) => { return token.symbol; }); + +/** + * The parser responsible for parsing a `prompt variable name`. + * E.g., `#selection` or `#workspace` variable. If the `:` character follows + * the variable name, the parser transitions to {@link PartialPromptVariableWithData} + * that is also able to parse the `data` part of the variable. E.g., the `#file` part + * of the `#file:/path/to/something.md` sequence. + */ +export class PartialPromptVariableName extends ParserBase { + constructor(token: Hash) { + super([token]); + } + + @assertNotConsumed + public accept(token: TSimpleToken): TAcceptTokenResult { + // if a `stop` character is encountered, finish the parsing process + if (STOP_CHARACTERS.includes(token.text)) { + try { + // if it is possible to convert current parser to `PromptVariable`, return success result + return { + result: 'success', + nextParser: this.asPromptVariable(), + wasTokenConsumed: false, + }; + } catch (error) { + // otherwise fail + return { + result: 'failure', + wasTokenConsumed: false, + }; + } finally { + // in any case this is an end of the parsing process + this.isConsumed = true; + } + } + + // if a `:` character is encountered, we might transition to {@link PartialPromptVariableWithData} + if (token instanceof Colon) { + this.isConsumed = true; + + // if there is only one token before the `:` character, it must be the starting + // `#` symbol, therefore fail because there is no variable name present + if (this.currentTokens.length <= 1) { + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // otherwise, if there are more characters after `#` available, + // we have a variable name, so we can transition to {@link PromptVariableWithData} + return { + result: 'success', + nextParser: new PartialPromptVariableWithData([...this.currentTokens, token]), + wasTokenConsumed: true, + }; + } + + // variables cannot have {@link INVALID_NAME_CHARACTERS} in their names + if (INVALID_NAME_CHARACTERS.includes(token.text)) { + this.isConsumed = true; + + return { + result: 'failure', + wasTokenConsumed: false, + }; + } + + // otherwise, a valid name character, so add it to the list of + // the current tokens and continue the parsing process + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + /** + * Try to convert current parser instance into a fully-parsed {@link PromptVariable} token. + * + * @throws if sequence of tokens received so far do not constitute a valid prompt variable, + * for instance, if there is only `1` starting `#` token is available. + */ + public asPromptVariable(): PromptVariable { + // if there is only one token before the stop character + // must be the starting `#` one), then fail + assert( + this.currentTokens.length > 1, + 'Cannot create a prompt variable out of incomplete token sequence.', + ); + + const firstToken = this.currentTokens[0]; + const lastToken = this.currentTokens[this.currentTokens.length - 1]; + + // render the characters above into strings, excluding the starting `#` character + const variableNameTokens = this.currentTokens.slice(1); + const variableName = variableNameTokens.map(pick('text')).join(''); + + return new PromptVariable( + new Range( + firstToken.range.startLineNumber, + firstToken.range.startColumn, + lastToken.range.endLineNumber, + lastToken.range.endColumn, + ), + variableName, + ); + } +} + +/** + * The parser responsible for parsing a `prompt variable name` with `data`. + * E.g., the `/path/to/something.md` part of the `#file:/path/to/something.md` sequence. + */ +export class PartialPromptVariableWithData extends ParserBase { + + constructor(tokens: readonly TSimpleToken[]) { + const firstToken = tokens[0]; + const lastToken = tokens[tokens.length - 1]; + + // sanity checks of our expectations about the tokens list + assert( + tokens.length > 2, + `Tokens list must contain at least 3 items, got '${tokens.length}'.`, + ); + assert( + firstToken instanceof Hash, + `The first token must be a '#', got '${firstToken} '.`, + ); + assert( + lastToken instanceof Colon, + `The last token must be a ':', got '${lastToken} '.`, + ); + + super([...tokens]); + } + + @assertNotConsumed + public accept(token: TSimpleToken): TAcceptTokenResult { + // if a `stop` character is encountered, finish the parsing process + if (STOP_CHARACTERS.includes(token.text)) { + // in any case, success of failure below, this is an end of the parsing process + this.isConsumed = true; + + const firstToken = this.currentTokens[0]; + const lastToken = this.currentTokens[this.currentTokens.length - 1]; + + // tokens representing variable name without the `#` character at the start and + // the `:` data separator character at the end + const variableNameTokens = this.currentTokens.slice(1, this.startTokensCount - 1); + // tokens representing variable data without the `:` separator character at the start + const variableDataTokens = this.currentTokens.slice(this.startTokensCount); + // compute the full range of the variable token + const fullRange = new Range( + firstToken.range.startLineNumber, + firstToken.range.startColumn, + lastToken.range.endLineNumber, + lastToken.range.endColumn, + ); + + // render the characters above into strings + const variableName = variableNameTokens.map(pick('text')).join(''); + const variableData = variableDataTokens.map(pick('text')).join(''); + + return { + result: 'success', + nextParser: new PromptVariableWithData( + fullRange, + variableName, + variableData, + ), + wasTokenConsumed: false, + }; + } + + // otherwise, token is a valid data character - the data can contain almost any character, + // including `:` and `#`, hence add it to the list of the current tokens and continue + this.currentTokens.push(token); + + return { + result: 'success', + nextParser: this, + wasTokenConsumed: true, + }; + } + + /** + * Try to convert current parser instance into a fully-parsed {@link asPromptVariableWithData} token. + */ + public asPromptVariableWithData(): PromptVariableWithData { + // tokens representing variable name without the `#` character at the start and + // the `:` data separator character at the end + const variableNameTokens = this.currentTokens.slice(1, this.startTokensCount - 1); + // tokens representing variable data without the `:` separator character at the start + const variableDataTokens = this.currentTokens.slice(this.startTokensCount); + + // render the characters above into strings + const variableName = variableNameTokens.map(pick('text')).join(''); + const variableData = variableDataTokens.map(pick('text')).join(''); + + const firstToken = this.currentTokens[0]; + const lastToken = this.currentTokens[this.currentTokens.length - 1]; + + return new PromptVariableWithData( + new Range( + firstToken.range.startLineNumber, + firstToken.range.startColumn, + lastToken.range.endLineNumber, + lastToken.range.endColumn, + ), + variableName, + variableData, + ); + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts index 5efc84d9a6c..fd04befbc57 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts @@ -3,118 +3,60 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +import { PromptVariableWithData } from './promptVariable.js'; import { assert } from '../../../../../../../base/common/assert.js'; import { IRange, Range } from '../../../../../../../editor/common/core/range.js'; import { BaseToken } from '../../../../../../../editor/common/codecs/baseToken.js'; -import { Word } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/word.js'; /** - * Start sequence for a file reference token in a prompt. + * Name of the variable. */ -const TOKEN_START: string = '#file:'; +const VARIABLE_NAME: string = 'file'; /** * Object represents a file reference token inside a chatbot prompt. */ -export class FileReference extends BaseToken { - /** - * Start sequence for a file reference token in a prompt. - */ - public static readonly TOKEN_START = TOKEN_START; - +export class FileReference extends PromptVariableWithData { constructor( range: Range, public readonly path: string, ) { - super(range); + super(range, VARIABLE_NAME, path); } /** - * Get full text of the file reference token. + * Create a {@link FileReference} from a {@link PromptVariableWithData} instance. + * @throws if variable name is not equal to {@link VARIABLE_NAME}. */ - public get text(): string { - return `${TOKEN_START}${this.path}`; - } - - /** - * Create a file reference token out of a generic `Word`. - * @throws if the word does not conform to the expected format or if - * the reference is an invalid `URI`. - */ - public static fromWord(word: Word): FileReference { - const { text } = word; - + public static from(variable: PromptVariableWithData) { assert( - text.startsWith(TOKEN_START), - `The reference must start with "${TOKEN_START}", got ${text}.`, + variable.name === VARIABLE_NAME, + `Variable name must be '${VARIABLE_NAME}', got '${variable.name}'.`, ); - const maybeReference = text.split(TOKEN_START); - - assert( - maybeReference.length === 2, - `The expected reference format is "${TOKEN_START}:filesystem-path", got ${text}.`, + return new FileReference( + variable.range, + variable.data, ); - - const [first, second] = maybeReference; - - assert( - first === '', - `The reference must start with "${TOKEN_START}", got ${first}.`, - ); - - assert( - // Note! this accounts for both cases when second is `undefined` or `empty` - // and we don't care about rest of the "falsy" cases here - !!second, - `The reference path must be defined, got ${second}.`, - ); - - const reference = new FileReference( - word.range, - second, - ); - - return reference; } /** * Check if this token is equal to another one. */ public override equals(other: T): boolean { - if (!super.sameRange(other.range)) { + if ((other instanceof FileReference) === false) { return false; } - if (!(other instanceof FileReference)) { - return false; - } - - return this.text === other.text; + return super.equals(other); } /** - * Get the range of the `link part` of the token (e.g., + * Get the range of the `link` part of the token (e.g., * the `/path/to/file.md` part of `#file:/path/to/file.md`). */ public get linkRange(): IRange | undefined { - if (this.path.length === 0) { - return undefined; - } - - const { range } = this; - return new Range( - range.startLineNumber, - range.startColumn + TOKEN_START.length, - range.endLineNumber, - range.endColumn, - ); - } - - /** - * Return a string representation of the token. - */ - public override toString(): string { - return `file-ref("${this.text}")${this.range}`; + return super.dataRange; } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditContext.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptToken.ts similarity index 51% rename from src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditContext.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptToken.ts index ff706d889be..38a9d69d5ac 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditContext.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptToken.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../../../nls.js'; -import { RawContextKey } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { BaseToken } from '../../../../../../../editor/common/codecs/baseToken.js'; -export const ctxNotebookHasEditorModification = new RawContextKey('chat.hasNotebookEditorModifications', undefined, localize('chat.hasNotebookEditorModifications', "The current Notebook editor contains chat modifications")); +/** + * Common base token that all chatbot `prompt` tokens should inherit from. + */ +export abstract class PromptToken extends BaseToken { } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptVariable.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptVariable.ts new file mode 100644 index 00000000000..449926faa3e --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptVariable.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromptToken } from './promptToken.js'; +import { assert } from '../../../../../../../base/common/assert.js'; +import { IRange, Range } from '../../../../../../../editor/common/core/range.js'; +import { BaseToken } from '../../../../../../../editor/common/codecs/baseToken.js'; +import { INVALID_NAME_CHARACTERS, STOP_CHARACTERS } from '../parsers/promptVariableParser.js'; + +/** + * All prompt variables start with `#` character. + */ +const START_CHARACTER: string = '#'; + +/** + * Character that separates name of a prompt variable from its data. + */ +const DATA_SEPARATOR: string = ':'; + +/** + * Represents a `#variable` token in a prompt text. + */ +export class PromptVariable extends PromptToken { + constructor( + range: Range, + /** + * The name of a prompt variable, excluding the `#` character at the start. + */ + public readonly name: string, + ) { + // sanity check of characters used in the provided variable name + for (const character of name) { + assert( + (INVALID_NAME_CHARACTERS.includes(character) === false) && + (STOP_CHARACTERS.includes(character) === false), + `Variable 'name' cannot contain character '${character}', got '${name}'.`, + ); + } + + super(range); + } + + /** + * Get full text of the token. + */ + public get text(): string { + return `${START_CHARACTER}${this.name}`; + } + + /** + * Check if this token is equal to another one. + */ + public override equals(other: T): boolean { + if (!super.sameRange(other.range)) { + return false; + } + + if ((other instanceof PromptVariable) === false) { + return false; + } + + if (this.text.length !== other.text.length) { + return false; + } + + return this.text === other.text; + } + + /** + * Return a string representation of the token. + */ + public override toString(): string { + return `${this.text}${this.range}`; + } +} + +/** + * Represents a {@link PromptVariable} with additional data token in a prompt text. + * (e.g., `#variable:/path/to/file.md`) + */ +export class PromptVariableWithData extends PromptVariable { + constructor( + fullRange: Range, + /** + * The name of the variable, excluding the starting `#` character. + */ + name: string, + + /** + * The data of the variable, excluding the starting {@link DATA_SEPARATOR} character. + */ + public readonly data: string, + ) { + super(fullRange, name); + + // sanity check of characters used in the provided variable data + for (const character of data) { + assert( + (STOP_CHARACTERS.includes(character) === false), + `Variable 'data' cannot contain character '${character}', got '${data}'.`, + ); + } + } + + /** + * Get full text of the token. + */ + public override get text(): string { + return `${START_CHARACTER}${this.name}${DATA_SEPARATOR}${this.data}`; + } + + /** + * Check if this token is equal to another one. + */ + public override equals(other: T): boolean { + if ((other instanceof PromptVariableWithData) === false) { + return false; + } + + return super.equals(other); + } + + /** + * Range of the `data` part of the variable. + */ + public get dataRange(): IRange | undefined { + const { range } = this; + + // calculate the start column number of the `data` part of the variable + const dataStartColumn = range.startColumn + + START_CHARACTER.length + this.name.length + + DATA_SEPARATOR.length; + + // create `range` of the `data` part of the variable + const result = new Range( + range.startLineNumber, + dataStartColumn, + range.endLineNumber, + range.endColumn, + ); + + // if the resulting range is empty, return `undefined` + // because there is no `data` part present in the variable + if (result.isEmpty()) { + return undefined; + } + + return result; + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts index 512c29e98c2..4f35897df0a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts @@ -11,7 +11,7 @@ import { CancellationError } from '../../../../../../base/common/errors.js'; import { PromptContentsProviderBase } from './promptContentsProviderBase.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { OpenFailed, NotPromptFile, ParseError, FolderReference } from '../../promptFileReferenceErrors.js'; +import { OpenFailed, NotPromptFile, ResolveError, FolderReference } from '../../promptFileReferenceErrors.js'; import { FileChangesEvent, FileChangeType, IFileService } from '../../../../../../platform/files/common/files.js'; /** @@ -81,7 +81,7 @@ export class FilePromptContentProvider extends PromptContentsProviderBase()); + private readonly onContentChangedEmitter = this._register(new Emitter()); /** * Event that fires when the prompt contents change. The event is either * a `VSBufferReadableStream` stream with changed contents or an instance of - * the `ParseError` class representing a parsing failure case. + * the `ResolveError` class representing a parsing failure case. * * `Note!` this field is meant to be used by the external consumers of the prompt * contents provider that the classes that extend this abstract class. @@ -112,7 +112,7 @@ export abstract class PromptContentsProviderBase< this.onContentChangedEmitter.fire(stream); }) .catch((error) => { - if (error instanceof ParseError) { + if (error instanceof ResolveError) { this.onContentChangedEmitter.fire(error); return; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts index 5d03ef2848c..3ff8c569b6d 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.d.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../../../base/common/uri.js'; -import { ParseError } from '../../promptFileReferenceErrors.js'; +import { ResolveError } from '../../promptFileReferenceErrors.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; @@ -27,9 +27,9 @@ export interface IPromptContentsProvider extends IDisposable { /** * Event that fires when the prompt contents change. The event is either a * {@linkcode VSBufferReadableStream} stream with changed contents or - * an instance of the {@linkcode ParseError} error. + * an instance of the {@linkcode ResolveError} error. */ onContentChanged( - callback: (streamOrError: VSBufferReadableStream | ParseError) => void, + callback: (streamOrError: VSBufferReadableStream | ResolveError) => void, ): IDisposable; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkDiagnosticsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkDiagnosticsProvider.ts new file mode 100644 index 00000000000..7d46c72dbbe --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkDiagnosticsProvider.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPromptsService } from '../service/types.js'; +import { IPromptFileReference } from '../parsers/types.js'; +import { assert } from '../../../../../../base/common/assert.js'; +import { NotPromptFile } from '../../promptFileReferenceErrors.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { IEditor } from '../../../../../../editor/common/editorCommon.js'; +import { ObjectCache } from '../../../../../../base/common/objectCache.js'; +import { TextModelPromptParser } from '../parsers/textModelPromptParser.js'; +import { Registry } from '../../../../../../platform/registry/common/platform.js'; +import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js'; +import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; +import { LifecyclePhase } from '../../../../../services/lifecycle/common/lifecycle.js'; +import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; +import { IWorkbenchContributionsRegistry, Extensions } from '../../../../../common/contributions.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../platform/markers/common/markers.js'; + +/** + * Unique ID of the markers provider class. + */ +const MARKERS_OWNER_ID = 'reusable-prompts-syntax'; + +/** + * Prompt links diagnostics provider for a single text model. + */ +class PromptLinkDiagnosticsProvider extends ObservableDisposable { + /** + * Reference to the current prompt syntax parser instance. + */ + private readonly parser: TextModelPromptParser; + + constructor( + private readonly editor: ITextModel, + @IMarkerService private readonly markerService: IMarkerService, + @IPromptsService private readonly promptsService: IPromptsService, + ) { + super(); + + this.parser = this.promptsService + .getSyntaxParserFor(this.editor) + .onUpdate(this.updateMarkers.bind(this)) + .onDispose(this.dispose.bind(this)) + .start(); + + // initialize markers + this.updateMarkers(); + } + + /** + * Update diagnostic markers for the current editor. + */ + private async updateMarkers() { + // ensure that parsing process is settled + await this.parser.allSettled(); + + // clean up all previously added markers + this.markerService.remove(MARKERS_OWNER_ID, [this.editor.uri]); + + const markers: IMarkerData[] = []; + for (const link of this.parser.references) { + const { topError, linkRange } = link; + + if (!topError || !linkRange) { + continue; + } + + const { originalError } = topError; + + // the `NotPromptFile` error is allowed because we allow users + // to include non-prompt file links in the prompt files + // note! this check also handles the `FolderReference` error + if (originalError instanceof NotPromptFile) { + continue; + } + + markers.push(toMarker(link)); + } + + this.markerService.changeOne( + MARKERS_OWNER_ID, + this.editor.uri, + markers, + ); + } +} + +/** + * Convert a prompt link with an issue to a marker data. + * + * @throws + * - if there is no link issue (e.g., `topError` undefined) + * - if there is no link range to highlight (e.g., `linkRange` undefined) + * - if the original error is of `NotPromptFile` type - we don't want to + * show diagnostic markers for non-prompt file links in the prompts + */ +const toMarker = ( + link: IPromptFileReference, +): IMarkerData => { + const { topError, linkRange } = link; + + // a sanity check because this function must be + // used only if these link attributes are present + assertDefined( + topError, + 'Top error must to be defined.', + ); + assertDefined( + linkRange, + 'Link range must to be defined.', + ); + + const { originalError } = topError; + assert( + !(originalError instanceof NotPromptFile), + 'Error must not be of "not prompt file" type.', + ); + + // `error` severity for the link itself, `warning` for any of its children + const severity = (topError.errorSubject === 'root') + ? MarkerSeverity.Error + : MarkerSeverity.Warning; + + return { + message: topError.localizedMessage, + severity, + ...linkRange, + }; +}; + +/** + * The class that manages creation and disposal of {@link PromptLinkDiagnosticsProvider} + * classes for each specific editor text model. + */ +export class PromptLinkDiagnosticsInstanceManager extends Disposable { + /** + * Currently available {@link PromptLinkDiagnosticsProvider} instances. + */ + private readonly providers: ObjectCache; + + constructor( + @IEditorService editorService: IEditorService, + @IInstantiationService initService: IInstantiationService, + @IConfigurationService configService: IConfigurationService, + ) { + super(); + + // cache of prompt marker providers + this.providers = this._register( + new ObjectCache((editor: ITextModel) => { + const parser: PromptLinkDiagnosticsProvider = initService.createInstance( + PromptLinkDiagnosticsProvider, + editor, + ); + + // this is a sanity check and the contract of the object cache, + // we must return a non-disposed object from this factory function + parser.assertNotDisposed( + 'Created prompt parser must not be disposed.', + ); + + return parser; + }), + ); + + // if the feature is disabled, do not create any providers + if (!PromptsConfig.enabled(configService)) { + return; + } + + // subscribe to changes of the active editor + this._register(editorService.onDidActiveEditorChange(() => { + const { activeTextEditorControl } = editorService; + if (!activeTextEditorControl) { + return; + } + + this.handleNewEditor(activeTextEditorControl); + })); + + // handle existing visible text editors + editorService + .visibleTextEditorControls + .forEach(this.handleNewEditor.bind(this)); + } + + /** + * Initialize a new {@link PromptLinkDiagnosticsProvider} for the given editor. + */ + private handleNewEditor(editor: IEditor): this { + const model = editor.getModel(); + if (!model) { + return this; + } + + // we support only `text editors` for now so filter out `diff` ones + if ('modified' in model || 'model' in model) { + return this; + } + + // enable this only for prompt file editors + if (!isPromptFile(model.uri)) { + return this; + } + + // note! calling `get` also creates a provider if it does not exist; + // and the provider is auto-removed when the model is disposed + this.providers.get(model); + + return this; + } +} + +// register the provider as a workbench contribution +Registry.as(Extensions.Workbench) + .registerWorkbenchContribution(PromptLinkDiagnosticsInstanceManager, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts index 7f026bdfff8..26f630845d5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptPathAutocompletion.ts @@ -17,14 +17,13 @@ import { LANGUAGE_SELECTOR } from '../constants.js'; import { IPromptsService } from '../service/types.js'; import { URI } from '../../../../../../base/common/uri.js'; -import { IPromptFileReference } from '../parsers/types.js'; -import { FileReference } from '../codecs/tokens/fileReference.js'; import { assertOneOf } from '../../../../../../base/common/types.js'; import { isWindows } from '../../../../../../base/common/platform.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { Position } from '../../../../../../editor/common/core/position.js'; +import { IPromptFileReference, IPromptReference } from '../parsers/types.js'; import { dirname, extUri } from '../../../../../../base/common/resources.js'; import { assert, assertNever } from '../../../../../../base/common/assert.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; @@ -57,7 +56,7 @@ type TTriggerCharacter = ':' | '.' | '/'; * Finds a file reference that suites the provided `position`. */ const findFileReference = ( - references: readonly IPromptFileReference[], + references: readonly IPromptReference[], position: Position, ): IPromptFileReference | undefined => { for (const reference of references) { @@ -69,7 +68,7 @@ const findFileReference = ( } // this ensures that we handle only the `#file:` references for now - if (!reference.text.startsWith(FileReference.TOKEN_START)) { + if (reference.subtype !== 'prompt') { return undefined; } @@ -245,21 +244,20 @@ export class PromptPathAutocompletion extends Disposable implements CompletionIt // when character is `:`, there must be no link present yet // otherwise the `:` was used in the middle of the link hence // we don't want to provide suggestions for that - if (character === ':' && linkRange !== undefined) { + if ((character === ':') && (linkRange !== undefined)) { return []; } // otherwise when the `.` character is present, it is inside the link part // of the reference, hence we always expect the link range to be present - if (character === '.' && linkRange === undefined) { + if ((character === '.') && (linkRange === undefined)) { return []; } const suggestions = await this.getFolderSuggestions(fileFolderUri); - // replacement range of the suggestions - // when character is `.` we want to also replace it, because we add - // the `./` at the beginning of all the relative paths + // replacement range for suggestions; when character is `.`, we want to also + // replace it, because we add `./` at the beginning of all the relative paths const startColumnOffset = (character === '.') ? 1 : 0; const range = { ...fileReference.range, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index 937c7e6b1bb..86b1833c96c 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -3,12 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../../../nls.js'; +import { TopError } from './topError.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ChatPromptCodec } from '../codecs/chatPromptCodec.js'; import { Emitter } from '../../../../../../base/common/event.js'; -import { assert } from '../../../../../../base/common/assert.js'; -import { IPromptFileReference, IResolveError } from './types.js'; import { FileReference } from '../codecs/tokens/fileReference.js'; import { ChatPromptDecoder } from '../codecs/chatPromptDecoder.js'; import { IRange } from '../../../../../../editor/common/core/range.js'; @@ -16,24 +14,17 @@ import { assertDefined } from '../../../../../../base/common/types.js'; import { IPromptContentsProvider } from '../contentProviders/types.js'; import { DeferredPromise } from '../../../../../../base/common/async.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { PromptVariableWithData } from '../codecs/tokens/promptVariable.js'; import { basename, extUri } from '../../../../../../base/common/resources.js'; +import { assert, assertNever } from '../../../../../../base/common/assert.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js'; import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; +import { IPromptFileReference, IPromptReference, IResolveError, ITopError } from './types.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js'; -import { OpenFailed, NotPromptFile, RecursiveReference, FolderReference, ParseError, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js'; - -/** - * Well-known localized error messages. - */ -const errorMessages = { - recursion: localize('chatPromptInstructionsRecursiveReference', 'Recursive reference found'), - fileOpenFailed: localize('chatPromptInstructionsFileOpenFailed', 'Failed to open file'), - streamOpenFailed: localize('chatPromptInstructionsStreamOpenFailed', 'Failed to open contents stream'), - brokenChild: localize('chatPromptInstructionsBrokenReference', 'Contains a broken reference that will be ignored'), -}; +import { OpenFailed, NotPromptFile, RecursiveReference, FolderReference, ResolveError } from '../../promptFileReferenceErrors.js'; /** * Error conditions that may happen during the file reference resolution. @@ -65,13 +56,13 @@ export abstract class BasePromptParser extend return this; } - private _errorCondition?: ParseError; + private _errorCondition?: ResolveError; /** * If file reference resolution fails, this attribute will be set * to an error instance that describes the error condition. */ - public get errorCondition(): ParseError | undefined { + public get errorCondition(): ResolveError | undefined { return this._errorCondition; } @@ -157,7 +148,10 @@ export abstract class BasePromptParser extend if (seenReferences.includes(this.uri.path)) { seenReferences.push(this.uri.path); - this._errorCondition = new RecursiveReference(this.uri, seenReferences); + this._errorCondition = new RecursiveReference( + this.uri, + seenReferences, + ); this._onUpdate.fire(); this.firstParseResult.complete(); @@ -196,7 +190,7 @@ export abstract class BasePromptParser extend * references recursion. */ private onContentsChanged( - streamOrError: VSBufferReadableStream | ParseError, + streamOrError: VSBufferReadableStream | ResolveError, seenReferences: string[], ): void { // dispose and cleanup the previously received stream @@ -209,7 +203,7 @@ export abstract class BasePromptParser extend this.disposeReferences(); // if an error received, set up the error condition and stop - if (streamOrError instanceof ParseError) { + if (streamOrError instanceof ResolveError) { this._errorCondition = streamOrError; this._onUpdate.fire(); @@ -225,8 +219,12 @@ export abstract class BasePromptParser extend // when some tokens received, process and store the references this.stream.on('data', (token) => { - if (token instanceof FileReference) { - this.onReference(token, [...seenReferences]); + if (token instanceof PromptVariableWithData) { + try { + this.onReference(FileReference.from(token), [...seenReferences]); + } catch (error) { + // no-op + } } // note! the `isURL` is a simple check and needs to be improved to truly @@ -345,7 +343,7 @@ export abstract class BasePromptParser extend /** * Get a list of immediate child references of the prompt. */ - public get references(): readonly IPromptFileReference[] { + public get references(): readonly IPromptReference[] { return [...this._references]; } @@ -353,8 +351,8 @@ export abstract class BasePromptParser extend * Get a list of all references of the prompt, including * all possible nested references its children may have. */ - public get allReferences(): readonly IPromptFileReference[] { - const result: IPromptFileReference[] = []; + public get allReferences(): readonly IPromptReference[] { + const result: IPromptReference[] = []; for (const reference of this.references) { result.push(reference); @@ -370,7 +368,7 @@ export abstract class BasePromptParser extend /** * Get list of all valid references. */ - public get allValidReferences(): readonly IPromptFileReference[] { + public get allValidReferences(): readonly IPromptReference[] { return this.allReferences // filter out unresolved references .filter((reference) => { @@ -399,38 +397,43 @@ export abstract class BasePromptParser extend .map(child => child.uri); } + /** + * Get list of errors for the direct links of the current reference. + */ + public get errors(): readonly ResolveError[] { + const childErrors: ResolveError[] = []; + + for (const reference of this.references) { + const { errorCondition } = reference; + + if (errorCondition && (!(errorCondition instanceof NotPromptFile))) { + childErrors.push(errorCondition); + } + } + + return childErrors; + } + /** * List of all errors that occurred while resolving the current * reference including all possible errors of nested children. */ - public get allErrors(): ParseError[] { - const result: ParseError[] = []; + public get allErrors(): readonly IResolveError[] { + const result: IResolveError[] = []; - // collect error conditions of all child references - const childErrorConditions = this - // get entire reference tree - .allReferences - // filter out children without error conditions or - // the ones that are non-prompt snippet files - .filter((childReference) => { - const { errorCondition } = childReference; + for (const reference of this.references) { + const { errorCondition } = reference; - return errorCondition && !(errorCondition instanceof NotPromptFile); - }) - // map to error condition objects - .map((childReference): ParseError => { - const { errorCondition } = childReference; + if (errorCondition && (!(errorCondition instanceof NotPromptFile))) { + result.push({ + originalError: errorCondition, + parentUri: this.uri, + }); + } - // `must` always be `true` because of the `filter` call above - assertDefined( - errorCondition, - `Error condition must be present for '${childReference.uri.path}'.`, - ); - - return errorCondition; - }); - - result.push(...childErrorConditions); + // recursively collect all possible errors of its children + result.push(...reference.allErrors); + } return result; } @@ -439,72 +442,48 @@ export abstract class BasePromptParser extend * The top most error of the current reference or any of its * possible child reference errors. */ - public get topError(): IResolveError | undefined { - // get all errors, including error of this object - const errors = []; + public get topError(): ITopError | undefined { if (this.errorCondition) { - errors.push(this.errorCondition); + return new TopError({ + errorSubject: 'root', + errorsCount: 1, + originalError: this.errorCondition, + }); } - errors.push(...this.allErrors); - // if no errors, nothing to do - if (errors.length === 0) { + const childErrors: ResolveError[] = [...this.errors]; + const nestedErrors: IResolveError[] = []; + for (const reference of this.references) { + nestedErrors.push(...reference.allErrors); + } + + if (childErrors.length === 0 && nestedErrors.length === 0) { return undefined; } + const firstDirectChildError = childErrors[0]; + const firstNestedChildError = nestedErrors[0]; + const hasDirectChildError = (firstDirectChildError !== undefined); - // if the first error is the error of the root reference, - // then return it as an `error` otherwise use `warning` - const [firstError, ...restErrors] = errors; - const isRootError = (firstError === this.errorCondition); + const firstChildError = (hasDirectChildError) + ? { + originalError: firstDirectChildError, + parentUri: this.uri, + } + : firstNestedChildError; - // if a child error - the error is somewhere in the nested references tree, - // then use message prefix to highlight that this is not a root error - const prefix = (!isRootError) - ? `${errorMessages.brokenChild}: ` - : ''; + const totalErrorsCount = childErrors.length + nestedErrors.length; - const moreSuffix = restErrors.length > 0 - ? `\n-\n +${restErrors.length} more error${restErrors.length > 1 ? 's' : ''}` - : ''; + const subject = (hasDirectChildError) + ? 'child' + : 'indirect-child'; - const errorMessage = this.getErrorMessage(firstError); - return { - isRootError, - message: `${prefix}${errorMessage}${moreSuffix}`, - }; - } - - /** - * Get message for the provided error condition object. - * - * @param error Error object that extends {@link ParseError}. - * @returns Error message. - */ - protected getErrorMessage(error: TError): string { - if (error instanceof OpenFailed) { - return `${errorMessages.fileOpenFailed} '${error.uri.path}'.`; - } - - if (error instanceof FailedToResolveContentsStream) { - return `${errorMessages.streamOpenFailed} '${error.uri.path}'.`; - } - - // if a recursion, provide the entire recursion path so users - // can use it for the debugging purposes - if (error instanceof RecursiveReference) { - const { recursivePath } = error; - - const recursivePathString = recursivePath - .map((path) => { - return basename(URI.file(path)); - }) - .join(' -> '); - - return `${errorMessages.recursion}:\n${recursivePathString}`; - } - - return error.message; + return new TopError({ + errorSubject: subject, + originalError: firstChildError.originalError, + parentUri: firstChildError.parentUri, + errorsCount: totalErrorsCount, + }); } /** @@ -546,7 +525,7 @@ export abstract class BasePromptParser extend /** * Prompt file reference object represents any file reference inside prompt - * text contents. For instanve the file variable(`#file:/path/to/file.md`) + * text contents. For instance the file variable(`#file:/path/to/file.md`) * or a markdown link(`[#file:file.md](/path/to/file.md)`). */ export class PromptFileReference extends BasePromptParser implements IPromptFileReference { @@ -575,7 +554,7 @@ export class PromptFileReference extends BasePromptParser { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/topError.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/topError.ts new file mode 100644 index 00000000000..97584c8a917 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/topError.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITopError } from './types.js'; +import { localize } from '../../../../../../nls.js'; +import { assert } from '../../../../../../base/common/assert.js'; +import { assertDefined } from '../../../../../../base/common/types.js'; +import { OpenFailed, RecursiveReference, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js'; + +/** + * The top-most error of the reference tree. + */ +export class TopError implements ITopError { + public readonly originalError: ITopError['originalError']; + public readonly errorSubject: ITopError['errorSubject']; + public readonly errorsCount: ITopError['errorsCount']; + public readonly parentUri: ITopError['parentUri']; + + constructor( + readonly options: Omit, + ) { + this.originalError = options.originalError; + this.errorSubject = options.errorSubject; + this.errorsCount = options.errorsCount; + this.parentUri = options.parentUri; + } + + public get localizedMessage(): string { + const { originalError, parentUri, errorSubject: subject, errorsCount } = this; + + assert( + errorsCount >= 1, + `Error count must be at least 1, got '${errorsCount}'.`, + ); + + // a note about how many more link issues are there + const moreIssuesLabel = (errorsCount > 1) + ? localize('workbench.reusable-prompts.top-error.more-issues-label', "\n(+{0} more issues)", errorsCount - 1) + : ''; + + if (subject === 'root') { + if (originalError instanceof OpenFailed) { + return localize( + 'workbench.reusable-prompts.top-error.open-failed', + "Cannot open '{0}'.{1}", + originalError.uri.path, + moreIssuesLabel, + ); + } + + if (originalError instanceof FailedToResolveContentsStream) { + return localize( + 'workbench.reusable-prompts.top-error.cannot-read', + "Cannot read '{0}'.{1}", + originalError.uri.path, + moreIssuesLabel, + ); + } + + if (originalError instanceof RecursiveReference) { + return localize( + 'workbench.reusable-prompts.top-error.recursive-reference', + "Recursion to itself.", + ); + } + + return originalError.message + moreIssuesLabel; + } + + // a sanity check - because the error subject is not `root`, the parent must set + assertDefined( + parentUri, + 'Parent URI must be defined for error of non-root link.', + ); + + const errorMessageStart = (subject === 'child') + ? localize( + 'workbench.reusable-prompts.top-error.child.direct', + "Contains", + ) + : localize( + 'workbench.reusable-prompts.top-error.child.indirect', + "Indirectly referenced prompt '{0}' contains", + parentUri.path, + ); + + const linkIssueName = (originalError instanceof RecursiveReference) + ? localize('recursive', "recursive") + : localize('broken', "broken"); + + return localize( + 'workbench.reusable-prompts.top-error.child.final-message', + "{0} a {1} link to '{2}' that will be ignored.{3}", + errorMessageStart, + linkIssueName, + originalError.uri.path, + moreIssuesLabel, + ); + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts index 325e5c3ec45..04f07f388b8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.d.ts @@ -4,39 +4,64 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../../../base/common/uri.js'; -import { ParseError } from '../../promptFileReferenceErrors.js'; +import { ResolveError } from '../../promptFileReferenceErrors.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; import { IRange, Range } from '../../../../../../editor/common/core/range.js'; /** - * Interface for a resolve error. + * A resolve error with a parent prompt URI, if any. */ export interface IResolveError { /** - * Localized error message. + * Original error instance. */ - message: string; + readonly originalError: ResolveError; /** - * Whether this error is for the root reference - * object, or for one of its possible children. + * URI of the parent that references this error. */ - isRootError: boolean; + readonly parentUri?: URI; } /** - * List of all available prompt reference types. + * Top most error of the reference tree. */ -type PromptReferenceTypes = 'file'; +export interface ITopError extends IResolveError { + /** + * Where does the error belong to: + * + * - `root` - the error is the top most error of the entire tree + * - `child` - the error is a child of the root error + * - `indirect-child` - the error is a child of a child of the root error + */ + readonly errorSubject: 'root' | 'child' | 'indirect-child'; + + /** + * Total number of all errors in the references tree, including the error + * of the current reference and all possible errors of its children. + */ + readonly errorsCount: number; + + /** + * Localized error message. + */ + readonly localizedMessage: string; +} /** - * Interface for a generic prompt reference. + * Base interface for a generic prompt reference. */ -export interface IPromptReference extends IDisposable { +interface IPromptReferenceBase extends IDisposable { /** - * Type of the prompt reference. + * Type of the prompt reference. E.g., `file`, `http`, `image`, etc. */ - readonly type: PromptReferenceTypes; + readonly type: string; + + /** + * Subtype of the prompt reference. For instance a `file` reference + * can be a `markdown link` or a prompt `#file:` variable reference. + */ + readonly subtype: string; /** * URI component of the associated with this reference. @@ -85,19 +110,24 @@ export interface IPromptReference extends IDisposable { * * See also {@linkcode resolveFailed}. */ - readonly errorCondition: ParseError | undefined; + readonly errorCondition: ResolveError | undefined; + + /** + * Get list of errors for the direct links of the current reference. + */ + readonly errors: readonly ResolveError[]; /** * List of all errors that occurred while resolving the current * reference including all possible errors of nested children. */ - readonly allErrors: readonly ParseError[]; + readonly allErrors: readonly IResolveError[]; /** * The top most error of the current reference or any of its * possible child reference errors. */ - readonly topError: IResolveError | undefined; + readonly topError: ITopError | undefined; /** * Direct references of the current reference. @@ -138,9 +168,20 @@ export interface IPromptReference extends IDisposable { } /** - * The special case of the {@linkcode IPromptReference} that pertains + * The special case of the {@linkcode IPromptReferenceBase} that pertains * to a file resource on the disk. */ -export interface IPromptFileReference extends IPromptReference { +export interface IPromptFileReference extends IPromptReferenceBase { readonly type: 'file'; + + /** + * Subtype of a file reference, - either a prompt `#file` variable, + * or a `markdown link` (e.g., `[caption](/path/to/file.md)`). + */ + readonly subtype: 'prompt' | 'markdown'; } + +/** + * List of all known prompt reference types. + */ +export type IPromptReference = IPromptFileReference; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index 4f0de893ce3..dc1ac08c94d 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -82,11 +82,11 @@ export class PromptsService extends Disposable implements IPromptsService { } public async listPromptFiles(): Promise { - const globalLocations = [this.userDataService.currentProfile.promptsHome]; + const userLocations = [this.userDataService.currentProfile.promptsHome]; const prompts = await Promise.all([ - this.fileLocator.listFilesIn(globalLocations, []) - .then(withType('global')), + this.fileLocator.listFilesIn(userLocations, []) + .then(withType('user')), this.fileLocator.listFiles([]) .then(withType('local')), ]); @@ -100,11 +100,11 @@ export class PromptsService extends Disposable implements IPromptsService { // sanity check to make sure we don't miss a new prompt type // added in the future assert( - type === 'local' || type === 'global', + type === 'local' || type === 'user', `Unknown prompt type '${type}'.`, ); - const prompts = (type === 'global') + const prompts = (type === 'user') ? [this.userDataService.currentProfile.promptsHome] : this.fileLocator.getConfigBasedSourceFolders(); @@ -116,7 +116,7 @@ export class PromptsService extends Disposable implements IPromptsService { * Utility to add a provided prompt `type` to a prompt URI. */ const addType = ( - type: 'local' | 'global', + type: 'local' | 'user', ): (uri: URI) => IPromptPath => { return (uri) => { return { uri, type: type }; @@ -127,7 +127,7 @@ const addType = ( * Utility to add a provided prompt `type` to a list of prompt URIs. */ const withType = ( - type: 'local' | 'global', + type: 'local' | 'user', ): (uris: readonly URI[]) => (readonly IPromptPath[]) => { return (uris) => { return uris diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts index cf8576067f2..1f29a5811b5 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/types.ts @@ -17,9 +17,9 @@ export const IPromptsService = createDecorator('IPromptsService /** * Supported prompt types. * - `local` means the prompt is a local file. -* - `global` means a "roamble" prompt file (similar to snippets). +* - `user` means a "roamble" prompt file (similar to snippets). */ -type TPromptsType = 'local' | 'global'; +type TPromptsType = 'local' | 'user'; /** * Represents a prompt path with its type. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts index 2d787402fbe..348b9b62801 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts @@ -13,7 +13,7 @@ import { IWorkspaceContextService } from '../../../../../../platform/workspace/c import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; /** - * Class to locate prompt files. + * Utility class to locate prompt files. */ export class PromptFilesLocator { constructor( diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index 27b3a2b8120..b6384cf0ad5 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -13,6 +13,8 @@ import { localize } from '../../../../../nls.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { SaveReason } from '../../../../common/editor.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; +import { CellUri } from '../../../notebook/common/notebookCommon.js'; +import { INotebookService } from '../../../notebook/common/notebookService.js'; import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; import { ChatModel } from '../../common/chatModel.js'; @@ -76,6 +78,7 @@ export class EditTool implements IToolImpl { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService, @ITextFileService private readonly textFileService: ITextFileService, + @INotebookService private readonly notebookService: INotebookService, ) { } async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { @@ -118,11 +121,21 @@ export class EditTool implements IToolImpl { kind: 'markdownContent', content: new MarkdownString(parameters.code + '\n````\n') }); - model.acceptResponseProgress(request, { - kind: 'textEdit', - edits: [], - uri - }); + const notebookUri = CellUri.parse(uri)?.notebook || uri; + // Signal start. + if (this.notebookService.hasSupportedNotebooks(notebookUri) && (this.notebookService.getNotebookTextModel(notebookUri))) { + model.acceptResponseProgress(request, { + kind: 'notebookEdit', + edits: [], + uri: notebookUri + }); + } else { + model.acceptResponseProgress(request, { + kind: 'textEdit', + edits: [], + uri + }); + } const editSession = this.chatEditingService.getEditingSession(model.sessionId); if (!editSession) { @@ -142,7 +155,12 @@ export class EditTool implements IToolImpl { }, }, token); - model.acceptResponseProgress(request, { kind: 'textEdit', uri, edits: [], done: true }); + // Signal end. + if (this.notebookService.hasSupportedNotebooks(notebookUri) && (this.notebookService.getNotebookTextModel(notebookUri))) { + model.acceptResponseProgress(request, { kind: 'notebookEdit', uri: notebookUri, edits: [], done: true }); + } else { + model.acceptResponseProgress(request, { kind: 'textEdit', uri, edits: [], done: true }); + } if (result?.errorMessage) { throw new Error(result.errorMessage); diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index e75fb61664a..d375686f553 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -135,6 +135,8 @@ function toToolKey(extensionIdentifier: ExtensionIdentifier, toolName: string) { return `${extensionIdentifier.value}/${toolName}`; } +const CopilotAgentModeTag = 'vscode_editing'; + export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.toolsExtensionPointHandler'; @@ -167,7 +169,14 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri continue; } - if (rawTool.tags?.some(tag => tag.startsWith('copilot_') || tag.startsWith('vscode_')) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { + if (rawTool.tags?.includes(CopilotAgentModeTag)) { + if (!isProposedApiEnabled(extension.description, 'languageModelToolsForAgent') && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { + logService.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with tag "${CopilotAgentModeTag}" without enabling 'languageModelToolsForAgent' proposal`); + continue; + } + } + + if (rawTool.tags?.some(tag => tag !== CopilotAgentModeTag && (tag.startsWith('copilot_') || tag.startsWith('vscode_'))) && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { logService.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with tags starting with "vscode_" or "copilot_"`); continue; } diff --git a/src/vs/workbench/contrib/chat/common/voiceChatService.ts b/src/vs/workbench/contrib/chat/common/voiceChatService.ts index 71ca69aecb1..bcf91b3a5b5 100644 --- a/src/vs/workbench/contrib/chat/common/voiceChatService.ts +++ b/src/vs/workbench/contrib/chat/common/voiceChatService.ts @@ -8,7 +8,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { rtrim } from '../../../../base/common/strings.js'; -import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IChatAgentService } from './chatAgents.js'; import { IChatModel } from './chatModel.js'; @@ -81,15 +81,17 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { private static readonly CHAT_AGENT_ALIAS = new Map([['vscode', 'code']]); - private readonly voiceChatInProgress = VoiceChatInProgress.bindTo(this.contextKeyService); + private readonly voiceChatInProgress: IContextKey; private activeVoiceChatSessions = 0; constructor( @ISpeechService private readonly speechService: ISpeechService, @IChatAgentService private readonly chatAgentService: IChatAgentService, - @IContextKeyService private readonly contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService ) { super(); + + this.voiceChatInProgress = VoiceChatInProgress.bindTo(contextKeyService); } private createPhrases(model?: IChatModel): Map { diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts index 0e888b7f864..6c89bb02221 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -5,7 +5,27 @@ import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallSpeechProviderForVoiceChatAction, HoldToVoiceChatInChatViewAction, ReadChatResponseAloud, StopReadAloud, StopReadChatItemAloud } from './actions/voiceChatActions.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; +import { FetchWebPageTool, FetchWebPageToolData } from './tools/fetchPageTool.js'; + +class NativeBuiltinToolsContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.nativeBuiltinTools'; + + constructor( + @ILanguageModelToolsService toolsService: ILanguageModelToolsService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const editTool = instantiationService.createInstance(FetchWebPageTool); + this._register(toolsService.registerToolData(FetchWebPageToolData)); + this._register(toolsService.registerToolImplementation(FetchWebPageToolData.id, editTool)); + } +} registerAction2(StartVoiceChatAction); registerAction2(InstallSpeechProviderForVoiceChatAction); @@ -23,3 +43,4 @@ registerAction2(StopReadChatItemAloud); registerAction2(StopReadAloud); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(NativeBuiltinToolsContribution.ID, NativeBuiltinToolsContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts b/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts new file mode 100644 index 00000000000..30418d3b03d --- /dev/null +++ b/src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../nls.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js'; +import { ITrustedDomainService } from '../../../url/browser/trustedDomainService.js'; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; + +export const InternalFetchWebPageToolId = 'vscode_fetchWebPage_internal'; +export const FetchWebPageToolData: IToolData = { + id: InternalFetchWebPageToolId, + displayName: 'Fetch Web Page', + tags: ['vscode_editing'], + modelDescription: localize('fetchWebPage.modelDescription', 'Fetches the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage.'), + userDescription: localize('fetchWebPage.userDescription', 'Fetch the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage.'), + inputSchema: { + type: 'object', + properties: { + urls: { + type: 'array', + items: { + type: 'string', + }, + description: localize('fetchWebPage.urlsDescription', 'An array of URLs to fetch content from.') + } + }, + required: ['urls'] + } +}; + +export class FetchWebPageTool implements IToolImpl { + private _alreadyApprovedDomains = new Set(); + + constructor( + @IWebContentExtractorService private readonly _readerModeService: IWebContentExtractorService, + @ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService, + ) { } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _token: CancellationToken): Promise { + const { valid } = this._parseUris((invocation.parameters as { urls?: string[] }).urls); + if (!valid.length) { + return { + content: [{ kind: 'text', value: localize('fetchWebPage.noValidUrls', 'No valid URLs provided.') }] + }; + } + + for (const uri of valid) { + if (!this._trustedDomainService.isValid(uri)) { + this._alreadyApprovedDomains.add(uri.toString(true)); + } + } + + const result = await this._readerModeService.extract(valid); + // Right now there's a bug when returning multiple text content parts so we're merging into one. + // When that's fixed we can use the helper function _getPromptPartForWebPageContents. + const value = result.map((content, index) => localize( + 'fetchWebPage.promptPart', + 'Below is the main content extracted from the webpage ({0}). Please read and analyze this content to assist with any follow-up questions:\n\n{1}', + valid[index].toString(), + content + )).join('\n\n---\n\n'); + return { content: [{ kind: 'text', value }] }; + } + + async prepareToolInvocation(parameters: any, token: CancellationToken): Promise { + const { invalid, valid } = this._parseUris(parameters.urls); + const urlsNeedingConfirmation = valid.filter(url => !this._trustedDomainService.isValid(url) && !this._alreadyApprovedDomains.has(url.toString(true))); + + const pastTenseMessage = invalid.length + ? invalid.length > 1 + ? new MarkdownString( + localize( + 'fetchWebPage.pastTenseMessage.plural', + 'Fetched {0} web pages, but the following were invalid URLs:\n\n{1}\n\n', valid.length, invalid.map(url => `- ${url}`).join('\n') + )) + : new MarkdownString( + localize( + 'fetchWebPage.pastTenseMessage.singular', + 'Fetched web page, but the following was an invalid URL:\n\n{0}\n\n', invalid[0] + )) + : new MarkdownString(); + pastTenseMessage.appendMarkdown(valid.length > 1 + ? localize('fetchWebPage.pastTenseMessageResult.plural', 'Fetched {0} web pages', valid.length) + : localize('fetchWebPage.pastTenseMessageResult.singular', 'Fetched [web page]({0})', valid[0].toString()) + ); + + const result: IPreparedToolInvocation = { + invocationMessage: valid.length > 1 + ? new MarkdownString(localize('fetchWebPage.invocationMessage.plural', 'Fetching {0} web pages', valid.length)) + : new MarkdownString(localize('fetchWebPage.invocationMessage.singular', 'Fetching [web page]({0})', valid[0].toString())), + pastTenseMessage + }; + + if (urlsNeedingConfirmation.length) { + const confirmationTitle = urlsNeedingConfirmation.length > 1 + ? localize('fetchWebPage.confirmationTitle.plural', 'Fetch untrusted web pages?') + : localize('fetchWebPage.confirmationTitle.singular', 'Fetch untrusted web page?'); + + const managedTrustedDomainsCommand = 'workbench.action.manageTrustedDomain'; + const confirmationMessage = new MarkdownString( + urlsNeedingConfirmation.length > 1 + ? urlsNeedingConfirmation.map(uri => `- ${uri.toString()}`).join('\n') + : urlsNeedingConfirmation[0].toString(), + { + isTrusted: { enabledCommands: [managedTrustedDomainsCommand] }, + supportThemeIcons: true + } + ); + + confirmationMessage.appendMarkdown( + '\n\n$(info)' + localize( + 'fetchWebPage.confirmationMessageManageTrustedDomains', + 'You can [manage your trusted domains]({0}) to skip this confirmation in the future.', + `command:${managedTrustedDomainsCommand}` + ) + ); + + result.confirmationMessages = { title: confirmationTitle, message: confirmationMessage }; + } + + return result; + } + + private _parseUris(urls?: string[]): { invalid: string[]; valid: URI[] } { + const invalidUrls: string[] = []; + const validUrls: URI[] = []; + urls?.forEach(uri => { + try { + const uriObj = URI.parse(uri); + validUrls.push(uriObj); + } catch (e) { + invalidUrls.push(uri); + } + }); + + return { invalid: invalidUrls, valid: validUrls }; + } + + // private _getPromptPartForWebPageContents(webPageContents: string, uri: URI): IToolResultTextPart { + // return { + // kind: 'text', + // value: localize( + // 'fetchWebPage.promptPart', + // 'Below is the main content extracted from the webpage ({0}). Please read and analyze this content to assist with any follow-up questions:\n\n{1}', + // uri.toString(), + // webPageContents + // ) + // }; + // } +} diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts new file mode 100644 index 00000000000..0ac36cbdb45 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts @@ -0,0 +1,1873 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { adjustCellDiffAndOriginalModelBasedOnCellAddDelete, adjustCellDiffAndOriginalModelBasedOnCellMovements, adjustCellDiffForKeepingADeletedCell, adjustCellDiffForKeepingAnInsertedCell, adjustCellDiffForRevertingADeletedCell, adjustCellDiffForRevertingAnInsertedCell } from '../../browser/chatEditing/notebook/helpers.js'; +import { ICellDiffInfo } from '../../browser/chatEditing/notebook/notebookCellChanges.js'; +import { nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; +import { ObservablePromise, observableValue } from '../../../../../base/common/observable.js'; +import { CellEditType, CellKind, ICell, ICellEditOperation, NotebookCellsChangeType } from '../../../notebook/common/notebookCommon.js'; +import { ITextModel } from '../../../../../editor/common/model.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { hash } from '../../../../../base/common/hash.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; + +suite('ChatEditingModifiedNotebookEntry', function () { + suite('Keep Inserted Cell', function () { + + const keep = () => Promise.resolve(true); + const undo = () => Promise.resolve(true); + const diff = observableValue('cell1', nullDocumentDiff); + const appliedEdits: ICellEditOperation[] = []; + setup(() => { + appliedEdits.length = 0; + }); + ensureNoDisposablesAreLeakedInTestSuite(); + function createModifiedModel(id: string): ObservablePromise { + return `Modified:${id}` as any; + + } + function createOriginalModel(id: string): ObservablePromise { + return `Original:${id}` as any; + + } + function applyEdits(edits: ICellEditOperation[]): boolean { + appliedEdits.push(...edits); + return true; + } + + function createModifiedCellDiffInfo(modifiedCellIndex: number, originalCellIndex: number): ICellDiffInfo { + return { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:${originalCellIndex}`), originalCellIndex, + modifiedCellIndex, modifiedModel: createModifiedModel(`InsertedModified:${modifiedCellIndex}`), + }; + } + test('Keep first inserted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + ]; + + const result = adjustCellDiffForKeepingAnInsertedCell(0, + cellsDiffInfo, {} as any, + applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 0, cells: [{}], count: 0 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:0`), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel(`InsertedModified:0`), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Keep first inserted with multiple cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('2'), + }, + ]; + + const result = adjustCellDiffForKeepingAnInsertedCell(0, + cellsDiffInfo, {} as any, + applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 0, cells: [{}], count: 0 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('InsertedOriginal:0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('InsertedModified:0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 3, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('2'), + }, + ]); + }); + test('Keep second inserted with multiple cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('2'), + }, + ]; + + const result = adjustCellDiffForKeepingAnInsertedCell(2, + cellsDiffInfo, {} as any, + applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 2, cells: [{}], count: 0 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('InsertedOriginal:2'), originalCellIndex: 2, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('InsertedModified:2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 3, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('2'), + }, + ]); + }); + }); + + suite('Revert Inserted Cell', function () { + + const keep = () => Promise.resolve(true); + const undo = () => Promise.resolve(true); + const diff = observableValue('cell1', nullDocumentDiff); + const appliedEdits: ICellEditOperation[] = []; + setup(() => { + appliedEdits.length = 0; + }); + ensureNoDisposablesAreLeakedInTestSuite(); + function createModifiedModel(id: string): ObservablePromise { + return `Modified:${id}` as any; + + } + function createOriginalModel(id: string): ObservablePromise { + return `Original:${id}` as any; + + } + function applyEdits(edits: ICellEditOperation[]): boolean { + appliedEdits.push(...edits); + return true; + } + + test('Delete first inserted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + ]; + + const result = adjustCellDiffForRevertingAnInsertedCell(0, + cellsDiffInfo, + applyEdits); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 0, cells: [], count: 1 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Delete first inserted with multiple cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('2'), + }, + ]; + + const result = adjustCellDiffForRevertingAnInsertedCell(0, + cellsDiffInfo, + applyEdits); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 0, cells: [], count: 1 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + ]); + }); + test('Delete second inserted with multiple cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('2'), + }, + ]; + + const result = adjustCellDiffForRevertingAnInsertedCell(2, + cellsDiffInfo, + applyEdits); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 2, cells: [], count: 1 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + ]); + }); + test('Delete second inserted with multiple cells (subsequent inserts)', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 4, modifiedModel: createModifiedModel('4'), + }, + ]; + + const result = adjustCellDiffForRevertingAnInsertedCell(2, + cellsDiffInfo, + applyEdits); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 2, cells: [], count: 1 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('3'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('4'), + }, + ]); + }); + }); + + suite('Keep Deleted Cell', function () { + + const keep = () => Promise.resolve(true); + const undo = () => Promise.resolve(true); + const diff = observableValue('cell1', nullDocumentDiff); + const appliedEdits: ICellEditOperation[] = []; + setup(() => { + appliedEdits.length = 0; + }); + ensureNoDisposablesAreLeakedInTestSuite(); + function createModifiedModel(id: string): ObservablePromise { + return `Modified:${id}` as any; + + } + function createOriginalModel(id: string): ObservablePromise { + return `Original:${id}` as any; + + } + function applyEdits(edits: ICellEditOperation[]): boolean { + appliedEdits.push(...edits); + return true; + } + + test('Keep first deleted cell', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + ]; + + const result = adjustCellDiffForKeepingADeletedCell(0, + cellsDiffInfo, + applyEdits); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 0, cells: [], count: 1 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 0, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Keep second deleted cell', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + ]; + + const result = adjustCellDiffForKeepingADeletedCell(1, + cellsDiffInfo, + applyEdits); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 1, cells: [], count: 1 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + + test('Keep first deleted with multiple cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('2'), + }, + ]; + + const result = adjustCellDiffForKeepingADeletedCell(1, + cellsDiffInfo, + applyEdits); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 1, cells: [], count: 1 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('2'), + }, + ]); + }); + }); + + suite('Revert Deleted Cell', function () { + + const keep = () => Promise.resolve(true); + const undo = () => Promise.resolve(true); + const diff = observableValue('cell1', nullDocumentDiff); + const appliedEdits: ICellEditOperation[] = []; + setup(() => { + appliedEdits.length = 0; + }); + ensureNoDisposablesAreLeakedInTestSuite(); + function createModifiedModel(id: string): ObservablePromise { + return `Modified:${id}` as any; + + } + function createOriginalModel(id: string): ObservablePromise { + return `Original:${id}` as any; + + } + function applyEdits(edits: ICellEditOperation[]): boolean { + appliedEdits.push(...edits); + return true; + } + function createModifiedCellDiffInfo(modifiedCellIndex: number, originalCellIndex: number): ICellDiffInfo { + return { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:${originalCellIndex}`), originalCellIndex, + modifiedCellIndex, modifiedModel: createModifiedModel(`InsertedModified:${modifiedCellIndex}`), + }; + } + + test('Revert first deleted cell', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + ]; + + const result = adjustCellDiffForRevertingADeletedCell(0, + cellsDiffInfo, + {} as any, + applyEdits, + createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 0, cells: [{}], count: 0 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('InsertedOriginal:0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('InsertedModified:0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Revert second deleted cell', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + ]; + + const result = adjustCellDiffForRevertingADeletedCell(1, + cellsDiffInfo, + {} as any, + applyEdits, + createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 0, cells: [{}], count: 0 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('InsertedOriginal:1'), originalCellIndex: 1, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('InsertedModified:0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + + test('Revert first deleted with multiple cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 4, modifiedModel: createModifiedModel('2'), + }, + ]; + + const result = adjustCellDiffForRevertingADeletedCell(1, + cellsDiffInfo, + {} as any, + applyEdits, + createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + { editType: CellEditType.Replace, index: 3, cells: [{}], count: 0 }, + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('New0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('InsertedOriginal:1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('InsertedModified:3'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 4, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: 5, modifiedModel: createModifiedModel('2'), + }, + ]); + }); + }); + + suite('Cell Addition', function () { + + const keep = () => Promise.resolve(true); + const undo = () => Promise.resolve(true); + const diff = observableValue('cell1', nullDocumentDiff); + const appliedEdits: ICellEditOperation[] = []; + setup(() => { + appliedEdits.length = 0; + }); + ensureNoDisposablesAreLeakedInTestSuite(); + function createModifiedModel(id: string): ObservablePromise { + return `Modified:${id}` as any; + + } + function createOriginalModel(id: string): ObservablePromise { + return `Original:${id}` as any; + + } + function applyEdits(edits: ICellEditOperation[]): boolean { + appliedEdits.push(...edits); + return true; + } + + function createICell(cellKind: CellKind, source: string): ICell { + const handle = hash(generateUuid()); + return { + uri: URI.parse(`file:///path/${handle}`), + handle, + cellKind, + language: cellKind === CellKind.Markup ? 'markdown' : 'python', + outputs: [], + metadata: {}, + getHashValue: () => { + return hash(`${handle}=>${cellKind}=>${source}`); + }, + getValue: () => { + return source; + }, + internalMetadata: {}, + } as any; + } + function createModifiedCellDiffInfo(modifiedCellIndex: number, originalCellIndex: number): ICellDiffInfo { + return { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:${originalCellIndex}`), originalCellIndex, + modifiedCellIndex, modifiedModel: createModifiedModel(`InsertedModified:${modifiedCellIndex}`), + }; + } + test('Insert a new cell into an unchanged notebook', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + ]; + + const cell = createICell(CellKind.Code, 'print("Hello World")'); + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([0, 0, [cell]], + cellsDiffInfo, 2, 2, applyEdits, createModifiedCellDiffInfo); + assert.deepStrictEqual(appliedEdits, [ + { + editType: CellEditType.Replace, + index: 0, + cells: [{ + cellKind: CellKind.Code, + language: 'python', + outputs: [], + mime: undefined, + metadata: {}, + internalMetadata: {}, + source: cell.getValue(), + }], count: 0 + } + ]); + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:0`), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel(`InsertedModified:0`), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 2, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Insert a new cell into an notebook with 3 cells deleted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('4'), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('4'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'modified', originalModel: createOriginalModel('6'), originalCellIndex: 6, + modifiedCellIndex: 4, modifiedModel: createModifiedModel('6'), + }, + ]; + const cell = createICell(CellKind.Code, 'print("Hello World")'); + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([2, 0, [cell]], + cellsDiffInfo, 5, 7, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + { + editType: CellEditType.Replace, + index: 4, + cells: [{ + cellKind: CellKind.Code, + language: 'python', + outputs: [], + mime: undefined, + metadata: {}, + internalMetadata: {}, + source: cell.getValue(), + }], count: 0 + } + ]); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('InsertedOriginal:4'), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('InsertedModified:2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('4'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('4'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 6, + modifiedCellIndex: 4, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'modified', originalModel: createOriginalModel('6'), originalCellIndex: 7, + modifiedCellIndex: 5, modifiedModel: createModifiedModel('6'), + }, + ]); + }); + test('Insert 2 new cells into an notebook with 3 cells deleted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + ]; + const cell1 = createICell(CellKind.Code, 'print("Hello World")'); + const cell2 = createICell(CellKind.Code, 'print("Foo Bar")'); + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([2, 0, [cell1, cell2]], + cellsDiffInfo, 4, 6, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + { + editType: CellEditType.Replace, + index: 4, + cells: [{ + cellKind: CellKind.Code, + language: 'python', + outputs: [], + mime: undefined, + metadata: {}, + internalMetadata: {}, + source: cell1.getValue(), + }, { + cellKind: CellKind.Code, + language: 'python', + outputs: [], + mime: undefined, + metadata: {}, + internalMetadata: {}, + source: cell2.getValue(), + }], count: 0 + } + ]); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:4`), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel(`InsertedModified:2`), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:5`), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel(`InsertedModified:3`), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 6, + modifiedCellIndex: 4, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 7, + modifiedCellIndex: 5, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Delete a cell from an unchanged notebook', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([0, 1, []], + cellsDiffInfo, 2, 2, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + { + editType: CellEditType.Replace, + index: 0, + cells: [], count: 1 + } + ]); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Delete last cell from an unchanged notebook', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([1, 1, []], + cellsDiffInfo, 2, 2, applyEdits, createModifiedCellDiffInfo); + assert.deepStrictEqual(appliedEdits, [ + { + editType: CellEditType.Replace, + index: 1, + cells: [], count: 1 + } + ]); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Delete a new cell from a notebook with 3 cells deleted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([1, 1, [ + // createICell(CellKind.Code, 'print("Hello World")') + ]], + cellsDiffInfo, 4, 6, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + ]); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Delete 2 cells from a notebook with 3 cells deleted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([1, 2, [ + ]], + cellsDiffInfo, 4, 6, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + { + editType: CellEditType.Replace, + index: 4, + cells: [], count: 1 + } + ]); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 4, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Delete 3 cells from a notebook with 3 cells deleted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'modified', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('6'), originalCellIndex: 6, + modifiedCellIndex: 4, modifiedModel: createModifiedModel('6'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([1, 3, [ + ]], + cellsDiffInfo, 5, 7, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(appliedEdits, [ + { + editType: CellEditType.Replace, + index: 1, + cells: [], count: 1 + }, + { + editType: CellEditType.Replace, + index: 5, + cells: [], count: 1 + } + ]); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('6'), originalCellIndex: 4, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('6'), + }, + ]); + }); + }); + + suite('Cell Movements', function () { + + const keep = () => Promise.resolve(true); + const undo = () => Promise.resolve(true); + const diff = observableValue('cell1', nullDocumentDiff); + + ensureNoDisposablesAreLeakedInTestSuite(); + function createModifiedModel(id: string): ObservablePromise { + return `Modified:${id}` as any; + + } + function createOriginalModel(id: string): ObservablePromise { + return `Original:${id}` as any; + + } + test('Swap first two inserted cells in a previously empty notebook', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 0, length: 1, newIdx: 1 + }, cellsDiffInfo); + + assert.ok(result); + assert.strictEqual(result[1].length, 0); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Swap first two inserted cells in a notebook that had 2 cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 0, length: 1, newIdx: 1 + }, cellsDiffInfo); + + assert.ok(result); + assert.strictEqual(result[1].length, 0); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]); + }); + test('Move first inserted cell to the very bottom of notebook that had 2 cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 0, length: 1, newIdx: 3 + }, cellsDiffInfo); + + assert.ok(result); + assert.strictEqual(result[1].length, 0); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('3'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Move last cell to top of notebook after 2 cells were inserted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 3, length: 1, newIdx: 0 + }, cellsDiffInfo); + + assert.ok(result); + assert.deepStrictEqual(result[1], [ + { + editType: CellEditType.Move, + index: 1, + length: 1, + newIdx: 0 + } + ]); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('3'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('2'), + }, + ]); + }); + + test('Move second inserted cell to the very bottom of notebook that had 2 cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 1, length: 1, newIdx: 3 + }, cellsDiffInfo); + + assert.ok(result); + assert.strictEqual(result[1].length, 0); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('3'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Move second inserted cell to the second last position of notebook that had 2 cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 1, length: 1, newIdx: 2 + }, cellsDiffInfo); + + assert.ok(result); + assert.strictEqual(result[1].length, 0); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]); + }); + test('Move first cell to the last position of notebook that had 3 cells deleted from the middle', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 0, length: 1, newIdx: 2 + }, cellsDiffInfo); + + assert.ok(result); + assert.deepStrictEqual(result[1], [ + { + editType: CellEditType.Move, + index: 0, + length: 1, + newIdx: 5 + } + ]); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 5, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Move second cell to the last position of notebook that had 3 cells deleted from the middle', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 1, length: 1, newIdx: 2 + }, cellsDiffInfo); + + assert.ok(result); + assert.deepStrictEqual(result[1], [ + { + editType: CellEditType.Move, + index: 1, + length: 1, + newIdx: 5 + } + ]); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + + test('Move second cell to the last position of notebook that had 3 cells deleted from middle and 1 inserted in the middle', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('5'), + }, + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 1, length: 1, newIdx: 3 + }, cellsDiffInfo); + + assert.ok(result); + assert.deepStrictEqual(result[1], [ + { + editType: CellEditType.Move, + index: 1, + length: 1, + newIdx: 5 + } + ]); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Move last cell to the second position of notebook that had 3 cells deleted from middle and 1 inserted in the middle', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('5'), + }, + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 3, length: 1, newIdx: 1 + }, cellsDiffInfo); + + assert.ok(result); + assert.deepStrictEqual(result[1], [ + { + editType: CellEditType.Move, + index: 5, + length: 1, + newIdx: 1 + } + ]); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 2, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 5, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('New1'), + }, + ]); + }); + }); + +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts index 974e7f0f5fd..5fed1eb581a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts @@ -17,12 +17,21 @@ import { IChatEditingService } from '../../common/chatEditingService.js'; import { assertThrowsAsync, ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; import { MockChatVariablesService } from '../common/mockChatVariables.js'; -import { ChatAgentLocation, ChatAgentService, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js'; +import { ChatAgentService, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js'; import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; +import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { assertType } from '../../../../../base/common/types.js'; +import { isEqual } from '../../../../../base/common/resources.js'; +import { waitForState } from '../../../../../base/common/observable.js'; +import { INotebookService } from '../../../notebook/common/notebookService.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { ChatAgentLocation } from '../../common/constants.js'; function getAgentData(id: string) { return { @@ -44,6 +53,7 @@ suite('ChatEditingService', function () { const store = new DisposableStore(); let editingService: ChatEditingService; let chatService: IChatService; + let textModelService: ITextModelService; setup(function () { const collection = new ServiceCollection(); @@ -58,6 +68,11 @@ suite('ChatEditingService', function () { return Disposable.None; } }); + collection.set(INotebookService, new class extends mock() { + override hasSupportedNotebooks(resource: URI): boolean { + return false; + } + }); const insta = store.add(store.add(workbenchInstantiationService(undefined, store)).createChild(collection)); const value = insta.get(IChatEditingService); assert.ok(value instanceof ChatEditingService); @@ -74,6 +89,16 @@ suite('ChatEditingService', function () { }; store.add(chatAgentService.registerAgent('testAgent', { ...getAgentData('testAgent'), isDefault: true })); store.add(chatAgentService.registerAgentImplementation('testAgent', agent)); + + textModelService = insta.get(ITextModelService); + + const modelService = insta.get(IModelService); + + store.add(textModelService.registerTextModelContentProvider('test', { + async provideTextContent(resource) { + return modelService.createModel(resource.path.repeat(10), null, resource, false); + }, + })); }); teardown(() => { @@ -100,4 +125,38 @@ suite('ChatEditingService', function () { model.dispose(); }); + + test('create session, file entry & isCurrentlyBeingModifiedBy', async function () { + assert.ok(editingService); + + const uri = URI.from({ scheme: 'test', path: 'HelloWorld' }); + + const model = chatService.startSession(ChatAgentLocation.EditingSession, CancellationToken.None); + const session = await editingService.createEditingSession(model.sessionId, true); + + const chatRequest = model?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); + assertType(chatRequest.response); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }], done: false }); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); + + const entry = await waitForState(session.entries.map(value => value.find(a => isEqual(a.modifiedURI, uri)))); + + assert.ok(isEqual(entry.modifiedURI, uri)); + + await waitForState(entry.isCurrentlyBeingModifiedBy.map(value => value === chatRequest.response)); + assert.ok(entry.isCurrentlyBeingModifiedBy.get() === chatRequest.response); + + const unset = waitForState(entry.isCurrentlyBeingModifiedBy.map(res => res === undefined)); + + chatRequest.response.complete(); + + await unset; + + await entry.reject(undefined); + + session.dispose(); + model.dispose(); + }); + }); diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_but_edit_mode.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_but_edit_mode.0.snap new file mode 100644 index 00000000000..c746469fe1e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_but_edit_mode.0.snap @@ -0,0 +1,19 @@ +{ + parts: [ + { + range: { + start: 0, + endExclusive: 12 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 13 + }, + text: "@agent hello", + kind: "text" + } + ], + text: "@agent hello" +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts index 580fa44ce45..cec803118e1 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts @@ -9,7 +9,6 @@ import { ContextKeyExpression } from '../../../../../platform/contextkey/common/ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { ChatAgentService, IChatAgentData, IChatAgentImplementation } from '../../common/chatAgents.js'; -import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; const testAgentId = 'testAgent'; const testAgentData: IChatAgentData = { @@ -42,7 +41,7 @@ suite('ChatAgents', function () { let contextKeyService: TestingContextKeyService; setup(() => { contextKeyService = new TestingContextKeyService(); - chatAgentService = store.add(new ChatAgentService(contextKeyService, store.add(new TestStorageService()))); + chatAgentService = store.add(new ChatAgentService(contextKeyService)); }); test('registerAgent', async () => { diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index b8e38fcb6ce..9f5444ca5cd 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -16,11 +16,12 @@ import { TestInstantiationService } from '../../../../../platform/instantiation/ import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; import { IStorageService } from '../../../../../platform/storage/common/storage.js'; -import { ChatAgentLocation, ChatAgentService, IChatAgentService } from '../../common/chatAgents.js'; +import { ChatAgentService, IChatAgentService } from '../../common/chatAgents.js'; import { ChatModel, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, normalizeSerializableChatData, Response } from '../../common/chatModel.js'; import { ChatRequestTextPart } from '../../common/chatParserTypes.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { ChatAgentLocation } from '../../common/constants.js'; suite('ChatModel', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index 7a2714635a6..d85f3a17f38 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -18,6 +18,7 @@ import { ChatRequestParser } from '../../common/chatRequestParser.js'; import { IChatService } from '../../common/chatService.js'; import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; +import { ChatMode } from '../../common/constants.js'; import { ILanguageModelToolsService, IToolData } from '../../common/languageModelToolsService.js'; import { MockChatService } from './mockChatService.js'; import { MockChatVariablesService } from './mockChatVariables.js'; @@ -142,6 +143,16 @@ suite('ChatRequestParser', () => { await assertSnapshot(result); }); + test('agent but edit mode', async () => { + const agentsService = mockObject()({}); + agentsService.getAgentsByName.returns([getAgentWithSlashCommands([])]); + instantiationService.stub(IChatAgentService, agentsService as any); + + parser = instantiationService.createInstance(ChatRequestParser); + const result = parser.parseChatRequest('1', '@agent hello', undefined, { mode: ChatMode.Edit }); + await assertSnapshot(result); + }); + test('agent with question mark', async () => { const agentsService = mockObject()({}); agentsService.getAgentsByName.returns([getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])]); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 8334b23f3ba..8f950258be9 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -20,7 +20,7 @@ import { IStorageService } from '../../../../../platform/storage/common/storage. import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { ChatAgentLocation, ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js'; +import { ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js'; import { IChatModel, ISerializableChatData } from '../../common/chatModel.js'; import { IChatFollowup, IChatService } from '../../common/chatService.js'; import { ChatService } from '../../common/chatServiceImpl.js'; @@ -34,6 +34,7 @@ import { IExtensionService, nullExtensionDescription } from '../../../../service import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { TestContextService, TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { ChatAgentLocation } from '../../common/constants.js'; const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext'; const chatAgentWithUsedContext: IChatAgent = { diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 4797a66c721..df0c99daaf4 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -90,4 +90,9 @@ export class MockChatService implements IChatService { setChatSessionTitle(sessionId: string, title: string): void { throw new Error('Method not implemented.'); } + + unifiedViewEnabled = false; + isEditingLocation(location: ChatAgentLocation): boolean { + throw new Error('Method not implemented.'); + } } diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts index 9a59dc5e5ed..267af76a9f1 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ChatAgentLocation } from '../../common/chatAgents.js'; import { IChatRequestVariableData, IChatRequestVariableEntry } from '../../common/chatModel.js'; import { IParsedChatRequest } from '../../common/chatParserTypes.js'; import { IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js'; +import { ChatAgentLocation } from '../../common/constants.js'; export class MockChatVariablesService implements IChatVariablesService { _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts index dceacea29d5..b063215ec7d 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptCodec.test.ts @@ -45,11 +45,11 @@ export class TestChatPromptCodec extends TestDecoder { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - test('produces expected tokens', async () => { + test('• produces expected tokens', async () => { const test = testDisposables.add(new TestChatPromptCodec()); await test.run( - '#file:/etc/hosts some text\t\n for #file:./README.md\t testing\n ✔ purposes\n#file:LICENSE.md ✌ \t#file:.gitignore\n\n\n\t #file:/Users/legomushroom/repos/vscode ', + '#file:/etc/hosts some text\t\n for #file:./README.md\t testing\n ✔ purposes\n#file:LICENSE.md ✌ \t#file:.gitignore\n\n\n\t #file:/Users/legomushroom/repos/vscode \n\nsomething #file:\tsomewhere\n', [ new FileReference( new Range(1, 1, 1, 1 + 16), @@ -71,6 +71,10 @@ suite('ChatPromptCodec', () => { new Range(7, 5, 7, 5 + 38), '/Users/legomushroom/repos/vscode', ), + new FileReference( + new Range(9, 11, 9, 11 + 6), + '', + ), ], ); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts index 1f73d7febae..6f009d57468 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/chatPromptDecoder.test.ts @@ -46,7 +46,7 @@ export class TestChatPromptDecoder extends TestDecoder { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - test('produces expected tokens', async () => { + test('• produces expected tokens', async () => { const test = testDisposables.add( new TestChatPromptDecoder(), ); @@ -59,6 +59,7 @@ suite('ChatPromptDecoder', () => { '## Heading Title', ' \t#file:a/b/c/filename2.md\t🖖\t#file:other-file.md', ' [#file:reference.md](./reference.md)some text #file:/some/file/with/absolute/path.md', + 'text text #file: another text', ]; await test.run( @@ -86,6 +87,10 @@ suite('ChatPromptDecoder', () => { new Range(7, 48, 7, 48 + 38), '/some/file/with/absolute/path.md', ), + new FileReference( + new Range(8, 11, 8, 11 + 6), + '', + ), ], ); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/fileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/fileReference.test.ts index 4a48b3a850d..3478fd7d34d 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/fileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/tokens/fileReference.test.ts @@ -7,14 +7,16 @@ import assert from 'assert'; import { randomInt } from '../../../../../../../../base/common/numbers.js'; import { Range } from '../../../../../../../../editor/common/core/range.js'; import { assertDefined } from '../../../../../../../../base/common/types.js'; +import { BaseToken } from '../../../../../../../../editor/common/codecs/baseToken.js'; +import { PromptToken } from '../../../../../common/promptSyntax/codecs/tokens/promptToken.js'; import { FileReference } from '../../../../../common/promptSyntax/codecs/tokens/fileReference.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../../base/test/common/utils.js'; -import { BaseToken } from '../../../../../../../../editor/common/codecs/baseToken.js'; +import { PromptVariable, PromptVariableWithData } from '../../../../../common/promptSyntax/codecs/tokens/promptVariable.js'; suite('FileReference', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('`linkRange`', () => { + test('• linkRange', () => { const lineNumber = randomInt(100, 1); const columnStartNumber = randomInt(100, 1); const path = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.txt`; @@ -46,7 +48,7 @@ suite('FileReference', () => { ); }); - test('`path`', () => { + test('• path', () => { const lineNumber = randomInt(100, 1); const columnStartNumber = randomInt(100, 1); const link = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.txt`; @@ -67,7 +69,7 @@ suite('FileReference', () => { ); }); - test('extends `BaseToken`', () => { + test('• extends `PromptVariableWithData` and others', () => { const lineNumber = randomInt(100, 1); const columnStartNumber = randomInt(100, 1); const link = `/temp/test/file-${randomInt(Number.MAX_SAFE_INTEGER)}.txt`; @@ -81,6 +83,21 @@ suite('FileReference', () => { ); const fileReference = new FileReference(range, link); + assert( + fileReference instanceof PromptVariableWithData, + 'Must extend `PromptVariableWithData`.', + ); + + assert( + fileReference instanceof PromptVariable, + 'Must extend `PromptVariable`.', + ); + + assert( + fileReference instanceof PromptToken, + 'Must extend `PromptToken`.', + ); + assert( fileReference instanceof BaseToken, 'Must extend `BaseToken`.', diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts index 45bd8552489..d43123799e1 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/expectedReference.ts @@ -7,8 +7,8 @@ import assert from 'assert'; import { URI } from '../../../../../../../base/common/uri.js'; import { Range } from '../../../../../../../editor/common/core/range.js'; import { assertDefined } from '../../../../../../../base/common/types.js'; -import { ParseError } from '../../../../common/promptFileReferenceErrors.js'; -import { IPromptFileReference } from '../../../../common/promptSyntax/parsers/types.js'; +import { ResolveError } from '../../../../common/promptFileReferenceErrors.js'; +import { IPromptReference } from '../../../../common/promptSyntax/parsers/types.js'; import { TErrorCondition } from '../../../../common/promptSyntax/parsers/basePromptParser.js'; /** @@ -63,7 +63,7 @@ export class ExpectedReference { /** * Validate that the provided reference is equal to this object. */ - public validateEqual(other: IPromptFileReference) { + public validateEqual(other: IPromptReference) { const { uri, text, path, childrenOrError = [] } = this.options; const errorPrefix = `[${uri}] `; @@ -130,7 +130,7 @@ export class ExpectedReference { * Next validate children or error condition. */ - if (childrenOrError instanceof ParseError) { + if (childrenOrError instanceof ResolveError) { const error = childrenOrError; const { errorCondition } = other; assertDefined( @@ -139,8 +139,8 @@ export class ExpectedReference { ); assert( - errorCondition instanceof ParseError, - `${errorPrefix} Expected 'errorCondition' to be a 'ParseError'.`, + errorCondition instanceof ResolveError, + `${errorPrefix} Expected 'errorCondition' to be a 'ResolveError'.`, ); assert( diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index 14347ec2af3..8228a56d3cf 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -27,10 +27,14 @@ import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from '../../.. const mockConfigService = (value: T): IConfigurationService => { return mockService({ getValue(key?: string | IConfigurationOverrides) { - assert.strictEqual( - key, - PromptsConfig.CONFIG_KEY, - `Mocked service supports only one configuration key: '${PromptsConfig.CONFIG_KEY}'.`, + assert( + typeof key === 'string', + `Expected string configuration key, got '${typeof key}'.`, + ); + + assert( + [PromptsConfig.CONFIG_KEY, PromptsConfig.LOCATIONS_CONFIG_KEY].includes(key), + `Unsupported configuration key '${key}'.`, ); return value; diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index 0005bca091e..53def63d490 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -66,7 +66,6 @@ suite('VoiceChat', () => { class TestChatAgentService implements IChatAgentService { _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; - readonly onDidChangeToolsAgentModeEnabled = Event.None; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } @@ -86,10 +85,7 @@ suite('VoiceChat', () => { getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } agentHasDupeName(id: string): boolean { throw new Error('Method not implemented.'); } getChatTitle(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - readonly toolsAgentModeEnabled: boolean = false; - toggleToolsAgentMode(): void { - throw new Error('Method not implemented.'); - } + hasToolsAgent: boolean = false; hasChatParticipantDetectionProviders(): boolean { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts index 6c657fb9124..9a24f11cbb4 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts @@ -10,7 +10,7 @@ import { CancellationTokenSource } from '../../../../../base/common/cancellation import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../../editor/browser/editorBrowser.js'; import { IEditorContribution } from '../../../../../editor/common/editorCommon.js'; -import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; import { HasSpeechProvider, ISpeechService, SpeechToTextInProgress, SpeechToTextStatus } from '../../../speech/common/speechService.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { EditorOption } from '../../../../../editor/common/config/editorOptions.js'; @@ -187,18 +187,21 @@ export class EditorDictation extends Disposable implements IEditorContribution { return editor.getContribution(EditorDictation.ID); } - private readonly widget = this._register(new DictationWidget(this.editor, this.keybindingService)); - private readonly editorDictationInProgress = EDITOR_DICTATION_IN_PROGRESS.bindTo(this.contextKeyService); + private readonly widget: DictationWidget; + private readonly editorDictationInProgress: IContextKey; private readonly sessionDisposables = this._register(new MutableDisposable()); constructor( private readonly editor: ICodeEditor, @ISpeechService private readonly speechService: ISpeechService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IKeybindingService private readonly keybindingService: IKeybindingService + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService ) { super(); + + this.widget = this._register(new DictationWidget(this.editor, keybindingService)); + this.editorDictationInProgress = EDITOR_DICTATION_IN_PROGRESS.bindTo(contextKeyService); } async start(): Promise { diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index 540a6deec5f..e8aedfe3cc5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -33,9 +33,10 @@ import { LOG_MODE_ID, OUTPUT_MODE_ID } from '../../../../services/output/common/ import { SEARCH_RESULT_LANGUAGE_ID } from '../../../../services/search/common/search.js'; import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { ChatAgentLocation, IChatAgent, IChatAgentService } from '../../../chat/common/chatAgents.js'; +import { IChatAgent, IChatAgentService } from '../../../chat/common/chatAgents.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; +import { ChatAgentLocation } from '../../../chat/common/constants.js'; const $ = dom.$; diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index ff2710dcd2b..a896a8019fd 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -419,7 +419,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { } dom.append(tbody, $('tr', undefined, - $('td.tiw-metadata-key', undefined, 'tree-sitter scopes' as string), + $('td.tiw-metadata-key', undefined, 'tree-sitter tree' as string), $('td.tiw-metadata-value.tiw-metadata-scopes', undefined, ...scopes), )); @@ -428,7 +428,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { if (captures && captures.length > 0) { dom.append(tbody, $('tr', undefined, $('td.tiw-metadata-key', undefined, 'foreground'), - $('td.tiw-metadata-value', undefined, captures[captures.length - 1].name), + $('td.tiw-metadata-value', undefined, captures.map(cap => cap.name).join(' ')), )); } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 8241f44ff19..2bea83e98e1 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -361,7 +361,7 @@ export class SuggestEnabledInputWithHistory extends SuggestEnabledInput implemen @IConfigurationService configurationService: IConfigurationService ) { super(id, parent, suggestionProvider, ariaLabel, resourceHandle, suggestOptions, instantiationService, modelService, contextKeyService, languageFeaturesService, configurationService); - this.history = new HistoryNavigator(new Set(history), 100); + this.history = this._register(new HistoryNavigator(new Set(history), 100)); } public addToHistory(): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index 309745ca99d..550288cb0de 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -22,6 +22,7 @@ import { CommentContextKeys } from '../common/commentContextKeys.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { CommentsModel, ICommentsModel } from './commentsModel.js'; import { IModelService } from '../../../../editor/common/services/model.js'; +import { Schemas } from '../../../../base/common/network.js'; export const ICommentService = createDecorator('commentService'); @@ -90,6 +91,7 @@ export interface ICommentService { readonly onDidSetDataProvider: Event; readonly onDidDeleteDataProvider: Event; readonly onDidChangeCommentingEnabled: Event; + readonly onResourceHasCommentingRanges: Event; readonly isCommentingEnabled: boolean; readonly commentsModel: ICommentsModel; readonly lastActiveCommentcontroller: ICommentController | undefined; @@ -154,6 +156,9 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidChangeCommentingEnabled = this._register(new Emitter()); readonly onDidChangeCommentingEnabled = this._onDidChangeCommentingEnabled.event; + private readonly _onResourceHasCommentingRanges = this._register(new Emitter()); + readonly onResourceHasCommentingRanges = this._onResourceHasCommentingRanges.event; + private readonly _onDidChangeActiveCommentingRange: Emitter<{ range: Range; commentingRangesInfo: CommentingRanges; @@ -232,6 +237,10 @@ export class CommentService extends Disposable implements ICommentService { })); this._register(this.modelService.onModelAdded(model => { + // Excluded schemes + if ((model.uri.scheme === Schemas.vscodeSourceControl)) { + return; + } // Allows comment providers to cause their commenting ranges to be prefetched by opening text documents in the background. if (!this._commentingRangeResources.has(model.uri.toString())) { this.getDocumentComments(model.uri); @@ -240,11 +249,16 @@ export class CommentService extends Disposable implements ICommentService { } private _updateResourcesWithCommentingRanges(resource: URI, commentInfos: (ICommentInfo | null)[]) { + let addedResources = false; for (const comments of commentInfos) { if (comments && (comments.commentingRanges.ranges.length > 0 || comments.threads.length > 0)) { this._commentingRangeResources.add(resource.toString()); + addedResources = true; } } + if (addedResources) { + this._onResourceHasCommentingRanges.fire(); + } } private _handleConfiguration() { diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts index c18066cbe58..3efc58ca953 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts @@ -23,6 +23,7 @@ import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { URI } from '../../../../base/common/uri.js'; import { CommentThread, Comment } from '../../../../editor/common/languages.js'; import { IRange } from '../../../../editor/common/core/range.js'; +import { IAction } from '../../../../base/common/actions.js'; export class CommentsAccessibleView extends Disposable implements IAccessibleViewImplementation { readonly priority = 90; @@ -72,30 +73,33 @@ export class CommentThreadAccessibleView extends Disposable implements IAccessib class CommentsAccessibleContentProvider extends Disposable implements IAccessibleViewContentProvider { + public readonly actions: IAction[]; constructor( private readonly _commentsView: CommentsPanel, private readonly _focusedCommentNode: any, private readonly _menus: CommentsMenus, ) { super(); + + this.actions = [...this._menus.getResourceContextActions(this._focusedCommentNode)].filter(i => i.enabled).map(action => { + return { + ...action, + run: () => { + this._commentsView.focus(); + action.run({ + thread: this._focusedCommentNode.thread, + $mid: MarshalledId.CommentThread, + commentControlHandle: this._focusedCommentNode.controllerHandle, + commentThreadHandle: this._focusedCommentNode.threadHandle, + }); + } + }; + }); } readonly id = AccessibleViewProviderId.Comments; readonly verbositySettingKey = AccessibilityVerbositySettingId.Comments; readonly options = { type: AccessibleViewType.View }; - public actions = [...this._menus.getResourceContextActions(this._focusedCommentNode)].filter(i => i.enabled).map(action => { - return { - ...action, - run: () => { - this._commentsView.focus(); - action.run({ - thread: this._focusedCommentNode.thread, - $mid: MarshalledId.CommentThread, - commentControlHandle: this._focusedCommentNode.controllerHandle, - commentThreadHandle: this._focusedCommentNode.threadHandle, - }); - } - }; - }); + provideContent(): string { const commentNode = this._commentsView.focusedCommentNode; const content = this._commentsView.focusedCommentInfo?.toString(); diff --git a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts index 016ead15476..41ed56ce07f 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts @@ -44,15 +44,20 @@ export class CommentsFilters extends Disposable { private readonly _onDidChange: Emitter = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; + private readonly _showUnresolved: IContextKey; + private readonly _showResolved: IContextKey; + private readonly _sortBy: IContextKey; constructor(options: CommentsFiltersOptions, private readonly contextKeyService: IContextKeyService) { super(); + this._showUnresolved = CONTEXT_KEY_SHOW_UNRESOLVED.bindTo(this.contextKeyService); + this._showResolved = CONTEXT_KEY_SHOW_RESOLVED.bindTo(this.contextKeyService); + this._sortBy = CONTEXT_KEY_SORT_BY.bindTo(this.contextKeyService); this._showResolved.set(options.showResolved); this._showUnresolved.set(options.showUnresolved); this._sortBy.set(options.sortBy); } - private readonly _showUnresolved = CONTEXT_KEY_SHOW_UNRESOLVED.bindTo(this.contextKeyService); get showUnresolved(): boolean { return !!this._showUnresolved.get(); } @@ -63,7 +68,6 @@ export class CommentsFilters extends Disposable { } } - private _showResolved = CONTEXT_KEY_SHOW_RESOLVED.bindTo(this.contextKeyService); get showResolved(): boolean { return !!this._showResolved.get(); } @@ -74,7 +78,6 @@ export class CommentsFilters extends Disposable { } } - private _sortBy: IContextKey = CONTEXT_KEY_SORT_BY.bindTo(this.contextKeyService); get sortBy(): CommentsSortOrder { return this._sortBy.get() ?? CommentsSortOrder.ResourceAscending; } diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index e6c66b60174..971e079b14c 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -105,6 +105,7 @@ .comments-panel .comments-panel-container .tree-container .comment-thread-container .range { opacity: 0.8; + overflow: visible; } .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .text code { diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index f3b3fb7f8d7..d246fb9fdda 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -113,6 +113,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); this._register(this.customEditorLabelService.onDidChange(() => this.updateLabel())); + this._register(this.filesConfigurationService.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); } private onLabelEvent(scheme: string): void { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index f1879eac040..0965dcaf636 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -224,7 +224,7 @@ export class BreakpointsView extends ViewPane { const iconLabelContainer = dom.append(container, $('span.breakpoint-warning')); this.hintContainer = this._register(new IconLabel(iconLabelContainer, { supportIcons: true, hoverDelegate: { - showHover: (options, focus?) => this.hoverService.showHover({ content: options.content, target: this.hintContainer!.element }, focus), + showHover: (options, focus?) => this.hoverService.showInstantHover({ content: options.content, target: this.hintContainer!.element }, focus), delay: this.configurationService.getValue('workbench.hover.delay') } })); diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index 9a0a631edd9..cc2513d24a3 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -9,7 +9,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { Constants } from '../../../../base/common/uint.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Range } from '../../../../editor/common/core/range.js'; -import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; +import { IEditorContribution, IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; import { GlyphMarginLane, IModelDecorationOptions, IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from '../../../../editor/common/model.js'; import { localize } from '../../../../nls.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -116,7 +116,7 @@ export function createDecorationsForStackFrame(stackFrame: IStackFrame, isFocuse } export class CallStackEditorContribution extends Disposable implements IEditorContribution { - private decorations = this.editor.createDecorationsCollection(); + private decorations: IEditorDecorationsCollection; constructor( private readonly editor: ICodeEditor, @@ -125,6 +125,7 @@ export class CallStackEditorContribution extends Disposable implements IEditorCo @ILogService private readonly logService: ILogService, ) { super(); + this.decorations = this.editor.createDecorationsCollection(); const setDecorations = () => this.decorations.set(this.createCallStackDecorations()); this._register(Event.any(this.debugService.getViewModel().onDidFocusStackFrame, this.debugService.getModel().onDidChangeCallStack)(() => { diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 34ef1b0806b..285e99c61f9 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -30,7 +30,7 @@ import { EditOperation } from '../../../../editor/common/core/editOperation.js'; import { Position } from '../../../../editor/common/core/position.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; import { DEFAULT_WORD_REGEXP } from '../../../../editor/common/core/wordHelper.js'; -import { ScrollType } from '../../../../editor/common/editorCommon.js'; +import { IEditorDecorationsCollection, ScrollType } from '../../../../editor/common/editorCommon.js'; import { StandardTokenType } from '../../../../editor/common/encodedTokenAttributes.js'; import { InlineValue, InlineValueContext } from '../../../../editor/common/languages.js'; import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops } from '../../../../editor/common/model.js'; @@ -261,7 +261,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { private configurationWidget: FloatingEditorClickWidget | undefined; private readonly altListener = new MutableDisposable(); private altPressed = false; - private oldDecorations = this.editor.createDecorationsCollection(); + private oldDecorations: IEditorDecorationsCollection; private readonly displayedStore = new DisposableStore(); private editorHoverOptions: IEditorHoverOptions | undefined; private readonly debounceInfo: IFeatureDebounceInformation; @@ -281,6 +281,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @ILanguageFeatureDebounceService featureDebounceService: ILanguageFeatureDebounceService ) { + this.oldDecorations = this.editor.createDecorationsCollection(); this.debounceInfo = featureDebounceService.for(languageFeaturesService.inlineValuesProvider, 'InlineValues', { min: DEAFULT_INLINE_DEBOUNCE_DELAY }); this.hoverWidget = this.instantiationService.createInstance(DebugHoverWidget, this.editor); this.toDispose = [this.defaultHoverLockout, this.altListener, this.displayedStore]; diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index ddd5e8bdbc6..2d2a3ea5f63 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -23,6 +23,7 @@ import { ConfigurationChangedEvent, EditorOption } from '../../../../editor/comm import { IDimension } from '../../../../editor/common/core/dimension.js'; import { Position } from '../../../../editor/common/core/position.js'; import { Range } from '../../../../editor/common/core/range.js'; +import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import * as nls from '../../../../nls.js'; @@ -93,7 +94,7 @@ export class DebugHoverWidget implements IContentWidget { private tree!: AsyncDataTree; private showAtPosition: Position | null; private positionPreference: ContentWidgetPositionPreference[]; - private readonly highlightDecorations = this.editor.createDecorationsCollection(); + private readonly highlightDecorations: IEditorDecorationsCollection; private complexValueContainer!: HTMLElement; private complexValueTitle!: HTMLElement; private valueContainer!: HTMLElement; @@ -118,6 +119,7 @@ export class DebugHoverWidget implements IContentWidget { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { + this.highlightDecorations = this.editor.createDecorationsCollection(); this.toDispose = []; this.showAtPosition = null; diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 879fc915524..9a359f4f9d8 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -867,7 +867,7 @@ export class DebugService implements IDebugService { // the session, then start the test run again; tests have no notion of restarts. if (session.correlatedTestRun) { if (!session.correlatedTestRun.completedAt) { - this.testService.cancelTestRun(session.correlatedTestRun.id); + session.cancelCorrelatedTestRun(); await Event.toPromise(session.correlatedTestRun.onComplete); // todo@connor4312 is there any reason to wait for the debug session to // terminate? I don't think so, test extension should already handle any diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 107ead5e51a..3e63e489167 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -402,6 +402,16 @@ export class DebugSession implements IDebugSession, IDisposable { } } + /** + * Terminate any linked test run. + */ + cancelCorrelatedTestRun() { + if (this.correlatedTestRun && !this.correlatedTestRun.completedAt) { + this.didTerminateTestRun = true; + this.testService.cancelTestRun(this.correlatedTestRun.id); + } + } + /** * terminate the current debug adapter session */ @@ -415,8 +425,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (this._options.lifecycleManagedByParent && this.parentSession) { await this.parentSession.terminate(restart); } else if (this.correlatedTestRun && !this.correlatedTestRun.completedAt && !this.didTerminateTestRun) { - this.didTerminateTestRun = true; - this.testService.cancelTestRun(this.correlatedTestRun.id); + this.cancelCorrelatedTestRun(); } else if (this.raw) { if (this.raw.capabilities.supportsTerminateRequest && this._configuration.resolved.request === 'launch') { await this.raw.terminate(restart); diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 41f28eabfc2..b9ea3a41178 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -32,7 +32,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; -import { getTitleBarStyle, TitlebarStyle } from '../../../../platform/window/common/window.js'; +import { getWindowControlsStyle, WindowControlsStyle } from '../../../../platform/window/common/window.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { EditorTabsMode, IWorkbenchLayoutService, LayoutSettings, Parts } from '../../../services/layout/browser/layoutService.js'; import { CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_IN_DEBUG_MODE, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugConfiguration, IDebugService, State, VIEWLET_ID } from '../common/debug.js'; @@ -80,12 +80,12 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { this.$el = dom.$('div.debug-toolbar'); // Note: changes to this setting require a restart, so no need to listen to it. - const customTitleBar = getTitleBarStyle(this.configurationService) === TitlebarStyle.CUSTOM; + const customWindowControls = getWindowControlsStyle(this.configurationService) === WindowControlsStyle.CUSTOM; // Do not allow the widget to overflow or underflow window controls. // Use CSS calculations to avoid having to force layout with `.clientWidth` - const controlsOnLeft = customTitleBar && platform === Platform.Mac; - const controlsOnRight = customTitleBar && (platform === Platform.Windows || platform === Platform.Linux); + const controlsOnLeft = customWindowControls && platform === Platform.Mac; + const controlsOnRight = customWindowControls && (platform === Platform.Windows || platform === Platform.Linux); this.$el.style.transform = `translate( min( max(${controlsOnLeft ? '60px' : '0px'}, calc(-50% + (100vw * var(--x-position)))), @@ -94,8 +94,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { var(--y-position) )`; - - this.dragArea = dom.append(this.$el, dom.$('div.drag-area' + ThemeIcon.asCSSSelector(icons.debugGripper))); const actionBarContainer = dom.append(this.$el, dom.$('div.action-bar-container')); diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index a7dbf5cdc76..930812de0f9 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -155,7 +155,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.menu = menuService.createMenu(MenuId.DebugConsoleContext, contextKeyService); this._register(this.menu); - this.history = new HistoryNavigator(new Set(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]'))), 100); + this.history = this._register(new HistoryNavigator(new Set(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]'))), 100)); this.filter = new ReplFilter(); this.filter.filterQuery = filterText; this.multiSessionRepl = CONTEXT_MULTI_SESSION_REPL.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 8df2579316a..6ce5e3e38a9 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -412,6 +412,8 @@ export interface IDebugSession extends ITreeElement { removeReplExpressions(): void; addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise; appendToRepl(data: INewReplElementData): void; + /** Cancel any associated test run set through the DebugSessionOptions */ + cancelCorrelatedTestRun(): void; // session events readonly onDidEndAdapter: Event; diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index add32108f5f..8d3c433daac 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -195,6 +195,10 @@ export class MockSession implements IDebugSession { throw new Error('Method not implemented.'); } + cancelCorrelatedTestRun(): void { + + } + get compoundRoot(): DebugCompoundRoot | undefined { return undefined; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 8542167a79b..6c235d89393 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -42,7 +42,6 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { buttonForeground, buttonHoverBackground, editorBackground, textLinkActiveForeground, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; import { IEditorOpenContext } from '../../../common/editor.js'; import { ExtensionFeaturesTab } from './extensionFeaturesTab.js'; @@ -73,18 +72,15 @@ import { } from './extensionsActions.js'; import { Delegate } from './extensionsList.js'; import { ExtensionData, ExtensionsGridView, ExtensionsTree, getExtensions } from './extensionsViewer.js'; -import { ExtensionRecommendationWidget, ExtensionStatusWidget, ExtensionWidget, InstallCountWidget, RatingsWidget, RemoteBadgeWidget, SponsorWidget, VerifiedPublisherWidget, onClick } from './extensionsWidgets.js'; +import { ExtensionRecommendationWidget, ExtensionStatusWidget, ExtensionWidget, InstallCountWidget, RatingsWidget, RemoteBadgeWidget, SponsorWidget, PublisherWidget, onClick, ExtensionKindIndicatorWidget } from './extensionsWidgets.js'; import { ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtension, IExtensionContainer, IExtensionsWorkbenchService } from '../common/extensions.js'; import { ExtensionsInput, IExtensionEditorOptions } from '../common/extensionsInput.js'; -import { IExplorerService } from '../../files/browser/files.js'; import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from '../../markdown/browser/markdownDocumentRenderer.js'; import { IWebview, IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from '../../webview/browser/webview.js'; import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { VIEW_ID as EXPLORER_VIEW_ID } from '../../files/common/files.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { ByteSize, IFileService } from '../../../../platform/files/common/files.js'; @@ -161,11 +157,6 @@ interface IExtensionEditorTemplate { name: HTMLElement; preview: HTMLElement; builtin: HTMLElement; - publisher: HTMLElement; - publisherDisplayName: HTMLElement; - resource: HTMLElement; - installCount: HTMLElement; - rating: HTMLElement; description: HTMLElement; actionsAndStatusContainer: HTMLElement; extensionActionBar: ActionBar; @@ -257,10 +248,6 @@ export class ExtensionEditor extends EditorPane { @ILanguageService private readonly languageService: ILanguageService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IExplorerService private readonly explorerService: IExplorerService, - @IViewsService private readonly viewsService: IViewsService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IHoverService private readonly hoverService: IHoverService, ) { super(ExtensionEditor.ID, group, telemetryService, themeService, storageService); @@ -302,29 +289,33 @@ export class ExtensionEditor extends EditorPane { builtin.textContent = localize('builtin', "Built-in"); const subtitle = append(details, $('.subtitle')); - const publisher = append(append(subtitle, $('.subtitle-entry')), $('.publisher.clickable', { tabIndex: 0 })); - publisher.setAttribute('role', 'button'); - const publisherDisplayName = append(publisher, $('.publisher-name')); - const verifiedPublisherWidget = this.instantiationService.createInstance(VerifiedPublisherWidget, append(publisher, $('.verified-publisher')), false); + const subTitleEntryContainers: HTMLElement[] = []; - const resource = append(append(subtitle, $('.subtitle-entry.resource')), $('', { tabIndex: 0 })); - resource.setAttribute('role', 'button'); + const publisherContainer = append(subtitle, $('.subtitle-entry')); + subTitleEntryContainers.push(publisherContainer); + const publisherWidget = this.instantiationService.createInstance(PublisherWidget, publisherContainer, false); - const installCount = append(append(subtitle, $('.subtitle-entry')), $('span.install', { tabIndex: 0 })); - this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), installCount, localize('install count', "Install count"))); - const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCount, false); + const extensionKindContainer = append(subtitle, $('.subtitle-entry')); + subTitleEntryContainers.push(extensionKindContainer); + const extensionKindWidget = this.instantiationService.createInstance(ExtensionKindIndicatorWidget, extensionKindContainer, false); - const rating = append(append(subtitle, $('.subtitle-entry')), $('span.rating.clickable', { tabIndex: 0 })); - this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), rating, localize('rating', "Rating"))); - rating.setAttribute('role', 'link'); // #132645 - const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, rating, false); + const installCountContainer = append(subtitle, $('.subtitle-entry')); + subTitleEntryContainers.push(installCountContainer); + const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCountContainer, false); - const sponsorWidget = this.instantiationService.createInstance(SponsorWidget, append(subtitle, $('.subtitle-entry'))); + const ratingsContainer = append(subtitle, $('.subtitle-entry')); + subTitleEntryContainers.push(ratingsContainer); + const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, ratingsContainer, false); + + const sponsorContainer = append(subtitle, $('.subtitle-entry')); + subTitleEntryContainers.push(sponsorContainer); + const sponsorWidget = this.instantiationService.createInstance(SponsorWidget, sponsorContainer); const widgets: ExtensionWidget[] = [ remoteBadge, versionWidget, - verifiedPublisherWidget, + publisherWidget, + extensionKindWidget, installCountWidget, ratingsWidget, sponsorWidget, @@ -440,18 +431,23 @@ export class ExtensionEditor extends EditorPane { header, icon, iconContainer, - installCount, name, navbar, preview, - publisher, - publisherDisplayName, - resource, - rating, actionsAndStatusContainer, extensionActionBar, set extension(extension: IExtension) { extensionContainers.extension = extension; + let lastNonEmptySubtitleEntryContainer; + for (const subTitleEntryElement of subTitleEntryContainers) { + subTitleEntryElement.classList.remove('last-non-empty'); + if (subTitleEntryElement.children.length > 0) { + lastNonEmptySubtitleEntryContainer = subTitleEntryElement; + } + } + if (lastNonEmptySubtitleEntryContainer) { + lastNonEmptySubtitleEntryContainer.classList.add('last-non-empty'); + } }, set gallery(gallery: IGalleryExtension | null) { versionWidget.gallery = gallery; @@ -559,38 +555,8 @@ export class ExtensionEditor extends EditorPane { template.description.textContent = extension.description; - // subtitle - template.publisher.classList.toggle('clickable', !!extension.url); - template.publisherDisplayName.textContent = extension.publisherDisplayName; - template.publisher.parentElement?.classList.toggle('hide', !!extension.resourceExtension || extension.local?.source === 'resource'); - this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.publisher, localize('publisher', "Publisher ({0})", extension.publisher))); - - const location = extension.resourceExtension?.location ?? (extension.local?.source === 'resource' ? extension.local?.location : undefined); - template.resource.parentElement?.classList.toggle('hide', !location); - if (location) { - const workspaceFolder = this.contextService.getWorkspaceFolder(location); - if (workspaceFolder && extension.isWorkspaceScoped) { - template.resource.parentElement?.classList.add('clickable'); - this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.resource, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location))); - template.resource.textContent = localize('workspace extension', "Workspace Extension"); - this.transientDisposables.add(onClick(template.resource, () => { - this.viewsService.openView(EXPLORER_VIEW_ID, true).then(() => this.explorerService.select(location, true)); - })); - } else { - template.resource.parentElement?.classList.remove('clickable'); - this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.resource, location.path)); - template.resource.textContent = localize('local extension', "Local Extension"); - } - } - - template.installCount.parentElement?.classList.toggle('hide', !extension.url); - template.rating.parentElement?.classList.toggle('hide', !extension.url); - template.rating.classList.toggle('clickable', !!extension.url); - if (extension.url) { this.transientDisposables.add(onClick(template.name, () => this.openerService.open(URI.parse(extension.url!)))); - this.transientDisposables.add(onClick(template.rating, () => this.openerService.open(URI.parse(`${extension.url}&ssr=false#review-details`)))); - this.transientDisposables.add(onClick(template.publisher, () => this.extensionsWorkbenchService.openSearch(`publisher:"${extension.publisherDisplayName}"`))); } const manifest = await this.extensionManifest.get().promise; @@ -1105,7 +1071,7 @@ class AdditionalDetailsWidget extends Disposable { if (extension.url) { resources.push([localize('Marketplace', "Marketplace"), URI.parse(extension.url)]); } - if (extension.url && extension.supportUrl) { + if (extension.supportUrl) { try { resources.push([localize('issues', "Issues"), URI.parse(extension.supportUrl)]); } catch (error) {/* Ignore */ } @@ -1115,7 +1081,7 @@ class AdditionalDetailsWidget extends Disposable { resources.push([localize('repository', "Repository"), URI.parse(extension.repository)]); } catch (error) {/* Ignore */ } } - if (extension.url && extension.licenseUrl) { + if (extension.licenseUrl) { try { resources.push([localize('license', "License"), URI.parse(extension.licenseUrl)]); } catch (error) {/* Ignore */ } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts b/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts index 9d0f876a307..0b90d652d3f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts @@ -27,7 +27,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import Severity from '../../../../base/common/severity.js'; import { errorIcon, infoIcon, warningIcon } from './extensionsIcons.js'; -import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { OS } from '../../../../base/common/platform.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; @@ -292,7 +292,7 @@ class RuntimeStatusMarkdownRenderer extends Disposable implements IExtensionFeat highlightCircle.style.display = 'block'; tooltip.style.left = `${closestPoint.x + 24}px`; tooltip.style.top = `${closestPoint.y + 14}px`; - hoverDisposable.value = this.hoverService.showHover({ + hoverDisposable.value = this.hoverService.showInstantHover({ content: new MarkdownString(`${closestPoint.date}: ${closestPoint.count} requests`), target: tooltip, appearance: { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 0fb425ed79c..19926de3171 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,12 +8,12 @@ import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey, SortBy, FilterType } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EnablementState, IExtensionManagementServerService, IPublisherInfo, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType, EXTENSIONS_CATEGORY } from '../common/extensions.js'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType, EXTENSIONS_CATEGORY, AutoRestartConfigurationKey } from '../common/extensions.js'; import { InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js'; import { ExtensionsInput } from '../common/extensionsInput.js'; import { ExtensionEditor } from './extensionEditor.js'; @@ -79,6 +79,8 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri import { IConfigurationMigrationRegistry, Extensions as ConfigurationMigrationExtensions } from '../../../common/configuration.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; +import product from '../../../../platform/product/common/product.js'; +import { IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */); @@ -265,6 +267,12 @@ Registry.as(ConfigurationExtensions.Configuration) scope: ConfigurationScope.APPLICATION, included: isNative }, + [AutoRestartConfigurationKey]: { + type: 'boolean', + description: localize('autoRestart', "If activated, extensions will automatically restart following an update if the window is not in focus. There can be a data loss if you have open Notebooks or Custom Editors."), + default: false, + included: product.quality !== 'stable' + }, [UseUnpkgResourceApiConfigKey]: { type: 'boolean', description: localize('extensions.gallery.useUnpkgResourceApi', "When enabled, extensions to update are fetched from Unpkg service."), @@ -456,7 +464,7 @@ CommandsRegistry.registerCommand({ } } else { const vsix = URI.revive(arg); - await extensionsWorkbenchService.install(vsix, { installOnlyNewlyAddedFromExtensionPack: options?.installOnlyNewlyAddedFromExtensionPackVSIX, installGivenVersion: true }); + await extensionsWorkbenchService.install(vsix, { installGivenVersion: true }); } } catch (e) { onUnexpectedError(e); @@ -539,6 +547,9 @@ overrideActionForActiveExtensionEditorWebview(PasteAction, webview => webview.pa export const CONTEXT_HAS_LOCAL_SERVER = new RawContextKey('hasLocalServer', false); export const CONTEXT_HAS_REMOTE_SERVER = new RawContextKey('hasRemoteServer', false); export const CONTEXT_HAS_WEB_SERVER = new RawContextKey('hasWebServer', false); +const CONTEXT_GALLERY_SORT_CAPABILITIES = new RawContextKey('gallerySortCapabilities', ''); +const CONTEXT_GALLERY_FILTER_CAPABILITIES = new RawContextKey('galleryFilterCapabilities', ''); +const CONTEXT_GALLERY_ALL_REPOSITORY_SIGNED = new RawContextKey('galleryAllRepositorySigned', false); async function runAction(action: IAction): Promise { try { @@ -560,7 +571,8 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi constructor( @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, - @IContextKeyService contextKeyService: IContextKeyService, + @IExtensionGalleryManifestService extensionGalleryManifestService: IExtensionGalleryManifestService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IViewsService private readonly viewsService: IViewsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @@ -590,11 +602,22 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi hasWebServerContext.set(true); } + extensionGalleryManifestService.getExtensionGalleryManifest() + .then(extensionGalleryManifest => { + this.registerGalleryCapabilitiesContexts(extensionGalleryManifest); + this._register(extensionGalleryManifestService.onDidChangeExtensionGalleryManifest(extensionGalleryManifest => this.registerGalleryCapabilitiesContexts(extensionGalleryManifest))); + }); this.registerGlobalActions(); this.registerContextMenuActions(); this.registerQuickAccessProvider(); } + private async registerGalleryCapabilitiesContexts(extensionGalleryManifest: IExtensionGalleryManifest | null): Promise { + CONTEXT_GALLERY_SORT_CAPABILITIES.bindTo(this.contextKeyService).set(`_${extensionGalleryManifest?.capabilities.extensionQuery.sorting?.map(s => s.name)?.join('_')}_UpdateDate_`); + CONTEXT_GALLERY_FILTER_CAPABILITIES.bindTo(this.contextKeyService).set(`_${extensionGalleryManifest?.capabilities.extensionQuery.filtering?.map(s => s.name)?.join('_')}_`); + CONTEXT_GALLERY_ALL_REPOSITORY_SIGNED.bindTo(this.contextKeyService).set(!!extensionGalleryManifest?.capabilities?.signing?.allRepositorySigned); + } + private registerQuickAccessProvider(): void { if (this.extensionManagementServerService.localExtensionManagementServer || this.extensionManagementServerService.remoteExtensionManagementServer @@ -898,7 +921,8 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi if (requireReload) { notificationService.prompt( Severity.Info, - localize('InstallVSIXAction.successReload', "Completed installing extension from VSIX. Please reload Visual Studio Code to enable it."), + vsixs.length > 1 ? localize('InstallVSIXs.successReload', "Completed installing extensions. Please reload Visual Studio Code to enable them.") + : localize('InstallVSIXAction.successReload', "Completed installing extension. Please reload Visual Studio Code to enable it."), [{ label: localize('InstallVSIXAction.reloadNow', "Reload Now"), run: () => hostService.reload() @@ -908,7 +932,8 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi else if (requireRestart) { notificationService.prompt( Severity.Info, - localize('InstallVSIXAction.successRestart', "Completed installing extension from VSIX. Please restart extensions to enable it."), + vsixs.length > 1 ? localize('InstallVSIXs.successRestart', "Completed installing extensions. Please restart extensions to enable them.") + : localize('InstallVSIXAction.successRestart', "Completed installing extension. Please restart extensions to enable it."), [{ label: localize('InstallVSIXAction.restartExtensions', "Restart Extensions"), run: () => extensionsWorkbenchService.updateRunningExtensions() @@ -918,7 +943,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi else { notificationService.prompt( Severity.Info, - localize('InstallVSIXAction.successNoReload', "Completed installing extension."), + vsixs.length > 1 ? localize('InstallVSIXs.successNoReload', "Completed installing extensions.") : localize('InstallVSIXAction.successNoReload', "Completed installing extension."), [] ); } @@ -985,16 +1010,17 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); const showFeaturedExtensionsId = 'extensions.filter.featured'; + const featuresExtensionsWhenContext = ContextKeyExpr.and(CONTEXT_HAS_GALLERY, ContextKeyExpr.regex(CONTEXT_GALLERY_FILTER_CAPABILITIES.key, new RegExp(`_${FilterType.Featured}_`))); this.registerExtensionAction({ id: showFeaturedExtensionsId, title: localize2('showFeaturedExtensions', 'Show Featured Extensions'), category: ExtensionsLocalizedLabel, menu: [{ id: MenuId.CommandPalette, - when: CONTEXT_HAS_GALLERY + when: featuresExtensionsWhenContext }, { id: extensionsFilterSubMenu, - when: CONTEXT_HAS_GALLERY, + when: featuresExtensionsWhenContext, group: '1_predefined', order: 1, }], @@ -1065,7 +1091,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { submenu: extensionsCategoryFilterSubMenu, title: localize('filter by category', "Category"), - when: CONTEXT_HAS_GALLERY, + when: ContextKeyExpr.and(CONTEXT_HAS_GALLERY, ContextKeyExpr.regex(CONTEXT_GALLERY_FILTER_CAPABILITIES.key, new RegExp(`_${FilterType.Category}_`))), group: '2_categories', order: 1, }); @@ -1184,19 +1210,20 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); [ - { id: 'installs', title: localize('sort by installs', "Install Count"), precondition: BuiltInExtensionsContext.negate() }, - { id: 'rating', title: localize('sort by rating', "Rating"), precondition: BuiltInExtensionsContext.negate() }, - { id: 'name', title: localize('sort by name', "Name"), precondition: BuiltInExtensionsContext.negate() }, - { id: 'publishedDate', title: localize('sort by published date', "Published Date"), precondition: BuiltInExtensionsContext.negate() }, - { id: 'updateDate', title: localize('sort by update date', "Updated Date"), precondition: ContextKeyExpr.and(SearchMarketplaceExtensionsContext.negate(), RecommendedExtensionsContext.negate(), BuiltInExtensionsContext.negate()) }, - ].map(({ id, title, precondition }, index) => { + { id: 'installs', title: localize('sort by installs', "Install Count"), precondition: BuiltInExtensionsContext.negate(), sortCapability: SortBy.InstallCount }, + { id: 'rating', title: localize('sort by rating', "Rating"), precondition: BuiltInExtensionsContext.negate(), sortCapability: SortBy.WeightedRating }, + { id: 'name', title: localize('sort by name', "Name"), precondition: BuiltInExtensionsContext.negate(), sortCapability: SortBy.Title }, + { id: 'publishedDate', title: localize('sort by published date', "Published Date"), precondition: BuiltInExtensionsContext.negate(), sortCapability: SortBy.PublishedDate }, + { id: 'updateDate', title: localize('sort by update date', "Updated Date"), precondition: ContextKeyExpr.and(SearchMarketplaceExtensionsContext.negate(), RecommendedExtensionsContext.negate(), BuiltInExtensionsContext.negate()), sortCapability: 'UpdateDate' }, + ].map(({ id, title, precondition, sortCapability }, index) => { + const sortCapabilityContext = ContextKeyExpr.regex(CONTEXT_GALLERY_SORT_CAPABILITIES.key, new RegExp(`_${sortCapability}_`)); this.registerExtensionAction({ id: `extensions.sort.${id}`, title, - precondition: ContextKeyExpr.and(precondition, ContextKeyExpr.regex(ExtensionsSearchValueContext.key, /^@feature:/).negate()), + precondition: ContextKeyExpr.and(precondition, ContextKeyExpr.regex(ExtensionsSearchValueContext.key, /^@feature:/).negate(), sortCapabilityContext), menu: [{ id: extensionsSortSubMenu, - when: ContextKeyExpr.or(CONTEXT_HAS_GALLERY, DefaultViewsContext), + when: ContextKeyExpr.and(ContextKeyExpr.or(CONTEXT_HAS_GALLERY, DefaultViewsContext), sortCapabilityContext), order: index, }], toggled: ExtensionsSortByContext.isEqualTo(id), @@ -1512,7 +1539,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '0_install', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('extensionIsUnsigned')), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('extensionIsUnsigned'), CONTEXT_GALLERY_ALL_REPOSITORY_SIGNED), order: 1 }, run: async (accessor: ServicesAccessor, extensionId: string) => { @@ -1636,12 +1663,10 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi group: '1_copy', when: ContextKeyExpr.has('isGalleryExtension'), }, - run: async (accessor: ServicesAccessor, extensionId: string) => { + run: async (accessor: ServicesAccessor, _, extension: IExtensionArg) => { const clipboardService = accessor.get(IClipboardService); - const productService = accessor.get(IProductService); - if (productService.extensionsGallery?.itemUrl) { - const link = `${productService.extensionsGallery.itemUrl}?itemName=${extensionId}`; - await clipboardService.writeText(link); + if (extension.galleryLink) { + await clipboardService.writeText(extension.galleryLink); } } }); @@ -1663,7 +1688,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi title: localize('download VSIX', "Download VSIX"), menu: { id: MenuId.ExtensionContext, - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension')), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('isGalleryExtension')), order: this.productService.quality === 'stable' ? 0 : 1 }, run: async (accessor: ServicesAccessor, extensionId: string) => { @@ -1676,7 +1701,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi title: localize('download pre-release', "Download Pre-Release VSIX"), menu: { id: MenuId.ExtensionContext, - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.has('extensionHasPreReleaseVersion')), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.has('extensionHasPreReleaseVersion')), order: this.productService.quality === 'stable' ? 1 : 0 }, run: async (accessor: ServicesAccessor, extensionId: string) => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 8b062d1d47d..7b344ef9fb6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -72,6 +72,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IUpdateService } from '../../../../platform/update/common/update.js'; import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; import { IAuthenticationUsageService } from '../../../services/authentication/browser/authenticationUsageService.js'; +import { IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js'; export class PromptExtensionInstallFailureAction extends Action { @@ -212,9 +213,6 @@ export class PromptExtensionInstallFailureAction extends Action { if (!this.extension.gallery) { return undefined; } - if (!this.productService.extensionsGallery) { - return undefined; - } if (!this.extensionManagementServerService.localExtensionManagementServer && !this.extensionManagementServerService.remoteExtensionManagementServer) { return undefined; } @@ -233,7 +231,18 @@ export class PromptExtensionInstallFailureAction extends Action { if (targetPlatform === TargetPlatform.UNKNOWN) { return undefined; } - return URI.parse(`${this.productService.extensionsGallery.serviceUrl}/publishers/${this.extension.publisher}/vsextensions/${this.extension.name}/${this.version}/vspackage${targetPlatform !== TargetPlatform.UNDEFINED ? `?targetPlatform=${targetPlatform}` : ''}`); + + const [extension] = await this.galleryService.getExtensions([{ + ...this.extension.identifier, + version: this.version + }], { + targetPlatform + }, CancellationToken.None); + + if (!extension) { + return undefined; + } + return URI.parse(extension.assets.download.uri); } } @@ -412,6 +421,7 @@ export class InstallAction extends ExtensionAction { @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, + @IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService, ) { super('extensions.install', localize('install', "Install"), InstallAction.CLASS, false); this.hideOnDisabled = false; @@ -460,7 +470,7 @@ export class InstallAction extends ExtensionAction { return; } - if (this.extension.gallery && !this.extension.gallery.isSigned) { + if (this.extension.gallery && !this.extension.gallery.isSigned && (await this.extensionGalleryManifestService.getExtensionGalleryManifest())?.capabilities.signing?.allRepositorySigned) { const { result } = await this.dialogService.prompt({ type: Severity.Warning, message: localize('not signed', "'{0}' is an extension from an unknown source. Are you sure you want to install?", this.extension.displayName), @@ -1250,7 +1260,7 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['galleryExtensionHasPreReleaseVersion', extension.gallery?.hasPreReleaseVersion]); cksOverlay.push(['extensionHasPreReleaseVersion', extension.hasPreReleaseVersion]); cksOverlay.push(['extensionHasReleaseVersion', extension.hasReleaseVersion]); - cksOverlay.push(['extensionDisallowInstall', !!extension.deprecationInfo?.disallowInstall]); + cksOverlay.push(['extensionDisallowInstall', extension.isMalicious || extension.deprecationInfo?.disallowInstall]); cksOverlay.push(['isExtensionAllowed', allowedExtensionsService.isAllowed({ id: extension.identifier.id, publisherDisplayName: extension.publisherDisplayName }) === true]); cksOverlay.push(['isPreReleaseExtensionAllowed', allowedExtensionsService.isAllowed({ id: extension.identifier.id, publisherDisplayName: extension.publisherDisplayName, prerelease: true }) === true]); cksOverlay.push(['extensionIsUnsigned', extension.gallery && !extension.gallery.isSigned]); @@ -1438,7 +1448,8 @@ export class MenuItemExtensionAction extends ExtensionAction { const extensionArg: IExtensionArg = { id: this.extension.identifier.id, version: this.extension.version, - location: this.extension.local?.location + location: this.extension.local?.location, + galleryLink: this.extension.url }; await this.action.run(id, extensionArg); } @@ -2514,6 +2525,7 @@ export class ExtensionStatusAction extends ExtensionAction { @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, @IWorkbenchExtensionEnablementService private readonly workbenchExtensionEnablementService: IWorkbenchExtensionEnablementService, @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, + @IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService, ) { super('extensions.status', '', `${ExtensionStatusAction.CLASS} hide`, false); this._register(this.labelService.onDidChangeFormatters(() => this.update(), this)); @@ -2540,7 +2552,7 @@ export class ExtensionStatusAction extends ExtensionAction { return; } - if (this.extension.state === ExtensionState.Uninstalled && this.extension.gallery && !this.extension.gallery.isSigned) { + if (this.extension.state === ExtensionState.Uninstalled && this.extension.gallery && !this.extension.gallery.isSigned && (await this.extensionGalleryManifestService.getExtensionGalleryManifest())?.capabilities.signing?.allRepositorySigned) { this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('not signed tooltip', "This extension is not signed by the Extension Marketplace.")) }, true); return; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts index f7871ef4e2b..ea1bc04ede8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts @@ -23,6 +23,7 @@ export const syncEnabledIcon = registerIcon('extensions-sync-enabled', Codicon.s export const syncIgnoredIcon = registerIcon('extensions-sync-ignored', Codicon.syncIgnored, localize('syncIgnoredIcon', 'Icon to indicate that an extension is ignored when syncing.')); export const remoteIcon = registerIcon('extensions-remote', Codicon.remote, localize('remoteIcon', 'Icon to indicate that an extension is remote in the extensions view and editor.')); export const installCountIcon = registerIcon('extensions-install-count', Codicon.cloudDownload, localize('installCountIcon', 'Icon shown along with the install count in the extensions view and editor.')); +export const privateExtensionIcon = registerIcon('extensions-private', Codicon.lock, localize('lockIcon', 'Icon shown for private extensions in the extensions view and editor.')); export const ratingIcon = registerIcon('extensions-rating', Codicon.star, localize('ratingIcon', 'Icon shown along with the rating in the extensions view and editor.')); export const preReleaseIcon = registerIcon('extensions-pre-release', Codicon.versions, localize('preReleaseIcon', 'Icon shown for extensions having pre-release versions in extensions view and editor.')); export const sponsorIcon = registerIcon('extensions-sponsor', Codicon.heartFilled, localize('sponsorIcon', 'Icon used for sponsoring extensions in the extensions view and editor.')); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 8e2e5ab9691..97be5462cf6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -11,11 +11,10 @@ import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { IPagedRenderer } from '../../../../base/browser/ui/list/listPaging.js'; -import { Event } from '../../../../base/common/event.js'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService, IExtensionsViewState } from '../common/extensions.js'; import { ManageExtensionAction, ExtensionRuntimeStateAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ButtonWithDropDownExtensionAction, InstallDropdownAction, InstallingLabelAction, ButtonWithDropdownExtensionActionViewItem, DropDownExtensionAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction } from './extensionsActions.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; -import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionRuntimeStatusWidget, PreReleaseBookmarkWidget, VerifiedPublisherWidget } from './extensionsWidgets.js'; +import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionRuntimeStatusWidget, PreReleaseBookmarkWidget, PublisherWidget, ExtensionKindIndicatorWidget } from './extensionsWidgets.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; @@ -34,7 +33,6 @@ export interface ITemplateData { element: HTMLElement; icon: HTMLImageElement; name: HTMLElement; - publisherDisplayName: HTMLElement; description: HTMLElement; installCount: HTMLElement; ratings: HTMLElement; @@ -85,13 +83,12 @@ export class Renderer implements IPagedRenderer { const installCount = append(header, $('span.install-count')); const ratings = append(header, $('span.ratings')); const syncIgnore = append(header, $('span.sync-ignored')); + const extensionKindIndicator = append(header, $('span')); const activationStatus = append(header, $('span.activation-status')); const headerRemoteBadgeWidget = this.instantiationService.createInstance(RemoteBadgeWidget, header, false); const description = append(details, $('.description.ellipsis')); const footer = append(details, $('.footer')); - const publisher = append(footer, $('.author.ellipsis')); - const verifiedPublisherWidget = this.instantiationService.createInstance(VerifiedPublisherWidget, append(publisher, $(`.verified-publisher`)), true); - const publisherDisplayName = append(publisher, $('.publisher-name.ellipsis')); + const publisherWidget = this.instantiationService.createInstance(PublisherWidget, append(footer, $('.publisher-container')), true); const actionbar = new ActionBar(footer, { actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { if (action instanceof ButtonWithDropDownExtensionAction) { @@ -140,12 +137,13 @@ export class Renderer implements IPagedRenderer { iconRemoteBadgeWidget, extensionPackBadgeWidget, headerRemoteBadgeWidget, - verifiedPublisherWidget, + publisherWidget, extensionHoverWidget, this.instantiationService.createInstance(SyncIgnoredWidget, syncIgnore), this.instantiationService.createInstance(ExtensionRuntimeStatusWidget, this.extensionViewState, activationStatus), this.instantiationService.createInstance(InstallCountWidget, installCount, true), this.instantiationService.createInstance(RatingsWidget, ratings, true), + this.instantiationService.createInstance(ExtensionKindIndicatorWidget, extensionKindIndicator, true), ]; const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); @@ -153,7 +151,7 @@ export class Renderer implements IPagedRenderer { const disposable = combinedDisposable(...actions, ...widgets, actionbar, actionBarListener, extensionContainers); return { - root, element, icon, name, installCount, ratings, description, publisherDisplayName, disposables: [disposable], actionbar, + root, element, icon, name, installCount, ratings, description, disposables: [disposable], actionbar, extensionDisposables: [], set extension(extension: IExtension) { extensionContainers.extension = extension; @@ -170,7 +168,6 @@ export class Renderer implements IPagedRenderer { data.icon.src = ''; data.name.textContent = ''; data.description.textContent = ''; - data.publisherDisplayName.textContent = ''; data.installCount.style.display = 'none'; data.ratings.style.display = 'none'; data.extension = null; @@ -209,12 +206,6 @@ export class Renderer implements IPagedRenderer { data.name.textContent = extension.displayName; data.description.textContent = extension.description; - const updatePublisher = () => { - data.publisherDisplayName.textContent = !extension.resourceExtension && extension.local?.source !== 'resource' ? extension.publisherDisplayName : ''; - }; - updatePublisher(); - Event.filter(this.extensionsWorkbenchService.onChange, e => !!e && areSameExtensions(e.identifier, extension.identifier))(() => updatePublisher(), this, data.extensionDisposables); - data.installCount.style.display = ''; data.ratings.style.display = ''; data.extension = extension; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index ee75bba1ed6..9b840de9a61 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -5,7 +5,7 @@ import './media/extensionsViewlet.css'; import { localize, localize2 } from '../../../../nls.js'; -import { timeout, Delayer, Promises } from '../../../../base/common/async.js'; +import { timeout, Delayer } from '../../../../base/common/async.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { createErrorWithActions } from '../../../../base/common/errorMessage.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; @@ -16,9 +16,9 @@ import { append, $, Dimension, hide, show, DragAndDropObserver, trackFocus, addD import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, AutoRestartConfigurationKey } from '../common/extensions.js'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, AutoRestartConfigurationKey, ExtensionRuntimeActionType } from '../common/extensions.js'; import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from './extensionsActions.js'; -import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IExtensionManagementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from '../../../services/extensionManagement/common/extensionManagement.js'; import { ExtensionsInput } from '../common/extensionsInput.js'; import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView, UntrustedWorkspaceUnsupportedExtensionsView, UntrustedWorkspacePartiallySupportedExtensionsView, VirtualWorkspaceUnsupportedExtensionsView, VirtualWorkspacePartiallySupportedExtensionsView, DefaultPopularExtensionsView, DeprecatedExtensionsView, SearchMarketplaceExtensionsView, RecentlyUpdatedExtensionsView, OutdatedExtensionsView, StaticQueryExtensionsView, NONE_CATEGORY } from './extensionsViews.js'; @@ -42,14 +42,13 @@ import { ViewPane } from '../../../browser/parts/views/viewPane.js'; import { Query } from '../common/extensionQuery.js'; import { SuggestEnabledInput } from '../../codeEditor/browser/suggestEnabledInput/suggestEnabledInput.js'; import { alert } from '../../../../base/browser/ui/aria/aria.js'; -import { EXTENSION_CATEGORIES, ExtensionType } from '../../../../platform/extensions/common/extensions.js'; +import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { MementoObject } from '../../../common/memento.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from '../../../common/theme.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { VirtualWorkspaceContext, WorkbenchStateContext } from '../../../common/contextkeys.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { installLocalInRemoteIcon } from './extensionsIcons.js'; @@ -59,12 +58,11 @@ import { IPaneCompositePartService } from '../../../services/panecomposite/brows import { coalesce } from '../../../../base/common/arrays.js'; import { extractEditorsAndFilesDropData } from '../../../../platform/dnd/browser/dnd.js'; import { extname } from '../../../../base/common/resources.js'; -import { isMalicious } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { ILocalizedString } from '../../../../platform/action/common/action.js'; import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -972,14 +970,12 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { constructor( @IExtensionManagementService private readonly extensionsManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IHostService private readonly hostService: IHostService, @ILogService private readonly logService: ILogService, @INotificationService private readonly notificationService: INotificationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { - if (!this.environmentService.disableExtensions) { - this.loopCheckForMaliciousExtensions(); - } + this.loopCheckForMaliciousExtensions(); } private loopCheckForMaliciousExtensions(): void { @@ -988,31 +984,36 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { .then(() => this.loopCheckForMaliciousExtensions()); } - private checkForMaliciousExtensions(): Promise { - return this.extensionsManagementService.getExtensionsControlManifest().then(extensionsControlManifest => { - - return this.extensionsManagementService.getInstalled(ExtensionType.User).then(installed => { - const maliciousExtensions = installed.filter(e => isMalicious(e.identifier, extensionsControlManifest)); - - if (maliciousExtensions.length) { - return Promises.settled(maliciousExtensions.map(e => this.extensionsManagementService.uninstall(e).then(() => { - this.notificationService.prompt( - Severity.Warning, - localize('malicious warning', "We have uninstalled '{0}' which was reported to be problematic.", e.identifier.id), - [{ - label: localize('reloadNow', "Reload Now"), - run: () => this.hostService.reload() - }], - { - sticky: true, - priority: NotificationPriority.URGENT - } - ); - }))); - } else { - return Promise.resolve(undefined); + private async checkForMaliciousExtensions(): Promise { + try { + const maliciousExtensions: ILocalExtension[] = []; + let shouldRestartExtensions = false; + let shouldReloadWindow = false; + for (const extension of this.extensionsWorkbenchService.installed) { + if (extension.isMalicious && extension.local) { + maliciousExtensions.push(extension.local); + shouldRestartExtensions = shouldRestartExtensions || extension.runtimeState?.action === ExtensionRuntimeActionType.RestartExtensions; + shouldReloadWindow = shouldReloadWindow || extension.runtimeState?.action === ExtensionRuntimeActionType.ReloadWindow; } - }).then(() => undefined); - }, err => this.logService.error(err)); + } + if (maliciousExtensions.length) { + await this.extensionsManagementService.uninstallExtensions(maliciousExtensions.map(e => ({ extension: e, options: { remove: true } }))); + this.notificationService.prompt( + Severity.Warning, + localize('malicious warning', "The following extensions were found to be problematic and have been uninstalled: {0}", maliciousExtensions.map(e => e.identifier.id).join(', ')), + shouldRestartExtensions || shouldReloadWindow ? [{ + label: shouldRestartExtensions ? localize('restartNow', "Restart Extensions") : localize('reloadNow', "Reload Now"), + run: () => shouldRestartExtensions ? this.extensionsWorkbenchService.updateRunningExtensions() : this.hostService.reload() + }] : [], + { + sticky: true, + priority: NotificationPriority.URGENT + } + ); + } + + } catch (err) { + this.logService.error(err); + } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index cbb4d0cbd4e..dd378644f9c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -39,7 +39,7 @@ import { IAction, Action, Separator, ActionRunner } from '../../../../base/commo import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionUntrustedWorkspaceSupportType, ExtensionVirtualWorkspaceSupportType, IExtensionDescription, IExtensionIdentifier, isLanguagePackExtension } from '../../../../platform/extensions/common/extensions.js'; import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from '../../../../base/common/async.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; -import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 17bba8cc4fa..20325ef00d3 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IUserDataSyncEnablementService } from '../../../../platform/userDataSync/common/userDataSync.js'; -import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, ratingIcon, remoteIcon, sponsorIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, warningIcon } from './extensionsIcons.js'; +import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, privateExtensionIcon, ratingIcon, remoteIcon, sponsorIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, warningIcon } from './extensionsIcons.js'; import { registerColor, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; @@ -46,6 +46,10 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { extensionVerifiedPublisherIconColor, verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IExplorerService } from '../../files/browser/files.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { VIEW_ID as EXPLORER_VIEW_ID } from '../../files/common/files.js'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -71,17 +75,26 @@ export function onClick(element: HTMLElement, callback: () => void): IDisposable export class InstallCountWidget extends ExtensionWidget { + private readonly disposables = this._register(new DisposableStore()); + constructor( - private container: HTMLElement, + readonly container: HTMLElement, private small: boolean, + @IHoverService private readonly hoverService: IHoverService, ) { super(); - container.classList.add('extension-install-count'); this.render(); + + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.container.innerText = ''; + this.disposables.clear(); } render(): void { - this.container.innerText = ''; + this.clear(); if (!this.extension) { return; @@ -96,9 +109,18 @@ export class InstallCountWidget extends ExtensionWidget { return; } - append(this.container, $('span' + ThemeIcon.asCSSSelector(installCountIcon))); - const count = append(this.container, $('span.count')); + if (!this.small && !this.extension.url) { + return; + } + + const parent = this.small ? this.container : append(this.container, $('span.install', { tabIndex: 0 })); + append(parent, $('span' + ThemeIcon.asCSSSelector(installCountIcon))); + const count = append(parent, $('span.count')); count.textContent = installLabel; + + if (!this.small) { + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.container, localize('install count', "Install count"))); + } } static getInstallLabel(extension: IExtension, small: boolean): string | undefined { @@ -129,12 +151,14 @@ export class InstallCountWidget extends ExtensionWidget { export class RatingsWidget extends ExtensionWidget { - private readonly containerHover: IManagedHover; + private containerHover: IManagedHover | undefined; + private readonly disposables = this._register(new DisposableStore()); constructor( - private container: HTMLElement, + readonly container: HTMLElement, private small: boolean, - @IHoverService hoverService: IHoverService + @IHoverService private readonly hoverService: IHoverService, + @IOpenerService private readonly openerService: IOpenerService, ) { super(); container.classList.add('extension-ratings'); @@ -143,13 +167,17 @@ export class RatingsWidget extends ExtensionWidget { container.classList.add('small'); } - this.containerHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, '')); - this.render(); + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.container.innerText = ''; + this.disposables.clear(); } render(): void { - this.container.innerText = ''; + this.clear(); if (!this.extension) { return; @@ -167,51 +195,71 @@ export class RatingsWidget extends ExtensionWidget { return; } + if (!this.extension.url) { + return; + } + const rating = Math.round(this.extension.rating * 2) / 2; - this.containerHover.update(localize('ratedLabel', "Average rating: {0} out of 5", rating)); if (this.small) { append(this.container, $('span' + ThemeIcon.asCSSSelector(starFullIcon))); const count = append(this.container, $('span.count')); count.textContent = String(rating); } else { + const element = append(this.container, $('span.rating.clickable', { tabIndex: 0 })); for (let i = 1; i <= 5; i++) { if (rating >= i) { - append(this.container, $('span' + ThemeIcon.asCSSSelector(starFullIcon))); + append(element, $('span' + ThemeIcon.asCSSSelector(starFullIcon))); } else if (rating >= i - 0.5) { - append(this.container, $('span' + ThemeIcon.asCSSSelector(starHalfIcon))); + append(element, $('span' + ThemeIcon.asCSSSelector(starHalfIcon))); } else { - append(this.container, $('span' + ThemeIcon.asCSSSelector(starEmptyIcon))); + append(element, $('span' + ThemeIcon.asCSSSelector(starEmptyIcon))); } } if (this.extension.ratingCount) { - const ratingCountElemet = append(this.container, $('span', undefined, ` (${this.extension.ratingCount})`)); + const ratingCountElemet = append(element, $('span', undefined, ` (${this.extension.ratingCount})`)); ratingCountElemet.style.paddingLeft = '1px'; } + + this.containerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, '')); + this.containerHover.update(localize('ratedLabel', "Average rating: {0} out of 5", rating)); + element.setAttribute('role', 'link'); + if (this.extension.ratingUrl) { + this.disposables.add(onClick(element, () => this.openerService.open(URI.parse(this.extension!.ratingUrl!)))); + } } } + } -export class VerifiedPublisherWidget extends ExtensionWidget { +export class PublisherWidget extends ExtensionWidget { + + private element: HTMLElement | undefined; + private containerHover: IManagedHover | undefined; private readonly disposables = this._register(new DisposableStore()); - private readonly containerHover: IManagedHover; constructor( - private container: HTMLElement, + readonly container: HTMLElement, private small: boolean, - @IHoverService hoverService: IHoverService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IHoverService private readonly hoverService: IHoverService, @IOpenerService private readonly openerService: IOpenerService, ) { super(); - this.containerHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, '')); + this.render(); + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.element?.remove(); + this.disposables.clear(); } render(): void { - reset(this.container); - this.disposables.clear(); - if (!this.extension?.publisherDomain?.verified) { + this.clear(); + if (!this.extension) { return; } @@ -223,20 +271,45 @@ export class VerifiedPublisherWidget extends ExtensionWidget { return; } - const publisherDomainLink = URI.parse(this.extension.publisherDomain.link); - const verifiedPublisher = append(this.container, $('span.extension-verified-publisher.clickable')); - append(verifiedPublisher, renderIcon(verifiedPublisherIcon)); + this.element = append(this.container, $('.publisher')); + const publisherDisplayName = $('.publisher-name.ellipsis'); + publisherDisplayName.textContent = this.extension.publisherDisplayName; - if (!this.small) { - verifiedPublisher.tabIndex = 0; - this.containerHover.update(`Verified Domain: ${this.extension.publisherDomain.link}`); - verifiedPublisher.setAttribute('role', 'link'); + const verifiedPublisher = $('.verified-publisher'); + append(verifiedPublisher, $('span.extension-verified-publisher.clickable'), renderIcon(verifiedPublisherIcon)); - append(verifiedPublisher, $('span.extension-verified-publisher-domain', undefined, publisherDomainLink.authority.startsWith('www.') ? publisherDomainLink.authority.substring(4) : publisherDomainLink.authority)); - this.disposables.add(onClick(verifiedPublisher, () => this.openerService.open(publisherDomainLink))); + if (this.small) { + if (this.extension.publisherDomain) { + append(this.element, verifiedPublisher); + } + append(this.element, publisherDisplayName); + } else { + this.element.classList.toggle('clickable', !!this.extension.url); + this.element.setAttribute('role', 'button'); + this.element.tabIndex = 0; + + this.containerHover = this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('publisher', "Publisher ({0})", this.extension.publisherDisplayName))); + append(this.element, publisherDisplayName); + + if (this.extension.publisherDomain) { + append(this.element, verifiedPublisher); + const publisherDomainLink = URI.parse(this.extension.publisherDomain.link); + verifiedPublisher.tabIndex = 0; + verifiedPublisher.setAttribute('role', 'button'); + this.containerHover.update(localize('verified publisher', "This publisher has verified ownership of {0}", this.extension.publisherDomain.link)); + verifiedPublisher.setAttribute('role', 'link'); + + append(verifiedPublisher, $('span.extension-verified-publisher-domain', undefined, publisherDomainLink.authority.startsWith('www.') ? publisherDomainLink.authority.substring(4) : publisherDomainLink.authority)); + this.disposables.add(onClick(verifiedPublisher, () => this.openerService.open(publisherDomainLink))); + } + + if (this.extension.url) { + this.disposables.add(onClick(this.element, () => this.extensionsWorkbenchService.openSearch(`publisher:"${this.extension?.publisherDisplayName}"`))); + } } } + } export class SponsorWidget extends ExtensionWidget { @@ -244,7 +317,7 @@ export class SponsorWidget extends ExtensionWidget { private readonly disposables = this._register(new DisposableStore()); constructor( - private container: HTMLElement, + readonly container: HTMLElement, @IHoverService private readonly hoverService: IHoverService, @IOpenerService private readonly openerService: IOpenerService, ) { @@ -445,6 +518,73 @@ export class ExtensionPackCountWidget extends ExtensionWidget { } } +export class ExtensionKindIndicatorWidget extends ExtensionWidget { + + private element: HTMLElement | undefined; + + private readonly disposables = this._register(new DisposableStore()); + + constructor( + readonly container: HTMLElement, + private small: boolean, + @IHoverService private readonly hoverService: IHoverService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IExplorerService private readonly explorerService: IExplorerService, + @IViewsService private readonly viewsService: IViewsService, + ) { + super(); + this.render(); + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.element?.remove(); + this.disposables.clear(); + } + + render(): void { + this.clear(); + + if (this.small) { + return; + } + + if (!this.extension) { + return; + } + + if (this.extension?.private) { + this.element = append(this.container, $('.extension-kind-indicator')); + append(this.element, $('span' + ThemeIcon.asCSSSelector(privateExtensionIcon))); + if (!this.small) { + append(this.element, $('span.private-extension-label', undefined, localize('privateExtension', "Private Extension"))); + } + return; + } + + const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined); + if (!location) { + return; + } + + this.element = append(this.container, $('.extension-kind-indicator')); + const workspaceFolder = this.contextService.getWorkspaceFolder(location); + if (workspaceFolder && this.extension.isWorkspaceScoped) { + this.element.textContent = localize('workspace extension', "Workspace Extension"); + this.element.classList.add('clickable'); + this.element.setAttribute('role', 'button'); + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location))); + this.disposables.add(onClick(this.element, () => { + this.viewsService.openView(EXPLORER_VIEW_ID, true).then(() => this.explorerService.select(location, true)); + })); + } else { + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, location.path)); + this.element.textContent = localize('local extension', "Local Extension"); + } + } +} + export class SyncIgnoredWidget extends ExtensionWidget { private readonly disposables = this._register(new DisposableStore()); @@ -555,7 +695,7 @@ export class ExtensionHoverWidget extends ExtensionWidget { this.hover.value = this.hoverService.setupManagedHover({ delay: this.configurationService.getValue('workbench.hover.delay'), showHover: (options, focus) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ ...options, additionalClasses: ['extension-hover'], position: { @@ -595,8 +735,12 @@ export class ExtensionHoverWidget extends ExtensionWidget { } markdown.appendText(`\n`); + let addSeparator = false; + if (this.extension.private) { + markdown.appendMarkdown(`$(${privateExtensionIcon.id}) ${localize('privateExtension', "Private Extension")}`); + addSeparator = true; + } if (this.extension.state === ExtensionState.Installed) { - let addSeparator = false; const installLabel = InstallCountWidget.getInstallLabel(this.extension, true); if (installLabel) { if (addSeparator) { @@ -620,9 +764,9 @@ export class ExtensionHoverWidget extends ExtensionWidget { markdown.appendMarkdown(`$(${sponsorIcon.id}) [${localize('sponsor', "Sponsor")}](${this.extension.publisherSponsorLink})`); addSeparator = true; } - if (addSeparator) { - markdown.appendText(`\n`); - } + } + if (addSeparator) { + markdown.appendText(`\n`); } const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined); @@ -862,6 +1006,7 @@ export class ExtensionRecommendationWidget extends ExtensionWidget { export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hcDark: '#FF8E00', hcLight: textLinkForeground }, localize('extensionIconStarForeground', "The icon color for extension ratings."), false); export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hcDark: '#1d9271', hcLight: textLinkForeground }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), false); export const extensionSponsorIconColor = registerColor('extensionIcon.sponsorForeground', { light: '#B51E78', dark: '#D758B3', hcDark: null, hcLight: '#B51E78' }, localize('extensionIcon.sponsorForeground', "The icon color for extension sponsor."), false); +export const extensionPrivateBadgeBackground = registerColor('extensionIcon.privateForeground', { dark: '#ffffff60', light: '#00000060', hcDark: '#ffffff60', hcLight: '#00000060' }, localize('extensionIcon.private', "The icon color for private extensions.")); registerThemingParticipant((theme, collector) => { const extensionRatingIcon = theme.getColor(extensionRatingIconColor); @@ -877,4 +1022,9 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`); collector.addRule(`.extension-editor > .header > .details > .subtitle .sponsor ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`); + + const privateBadgeBackground = theme.getColor(extensionPrivateBadgeBackground); + if (privateBadgeBackground) { + collector.addRule(`.extension-private-badge { color: ${privateBadgeBackground}; }`); + } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 41bcd531cfa..efd8a2732f1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -63,11 +63,10 @@ import { areApiProposalsCompatible, isEngineValid } from '../../../../platform/e import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { ShowCurrentReleaseNotesActionId } from '../../update/common/update.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js'; interface IExtensionStateProvider { (extension: Extension): T; @@ -198,11 +197,7 @@ export class Extension implements IExtension { } get publisherUrl(): URI | undefined { - if (!this.productService.extensionsGallery || !this.gallery) { - return undefined; - } - - return resources.joinPath(URI.parse(this.productService.extensionsGallery.publisherUrl), this.publisher); + return this.gallery?.publisherLink ? URI.parse(this.gallery.publisherLink) : undefined; } get publisherDomain(): { link: string; verified: boolean } | undefined { @@ -217,6 +212,10 @@ export class Extension implements IExtension { return this.local ? this.local.manifest.version : this.latestVersion; } + get private(): boolean { + return this.local ? this.local.private : this.gallery ? this.gallery.private : false; + } + get pinned(): boolean { return !!this.local?.pinned; } @@ -230,11 +229,7 @@ export class Extension implements IExtension { } get url(): string | undefined { - if (!this.productService.extensionsGallery || !this.gallery) { - return undefined; - } - - return `${this.productService.extensionsGallery.itemUrl}?itemName=${this.publisher}.${this.name}`; + return this.gallery?.detailsLink; } get iconUrl(): string { @@ -297,7 +292,11 @@ export class Extension implements IExtension { return this.stateProvider(this); } - public isMalicious: boolean = false; + private malicious: boolean = false; + public get isMalicious(): boolean { + return this.malicious || this.enablementState === EnablementState.DisabledByMalicious; + } + public deprecationInfo: IDeprecationInfo | undefined; get installCount(): number | undefined { @@ -312,6 +311,10 @@ export class Extension implements IExtension { return this.gallery ? this.gallery.ratingCount : undefined; } + get ratingUrl(): string | undefined { + return this.gallery?.ratingLink; + } + get outdated(): boolean { try { if (!this.gallery || !this.local) { @@ -551,7 +554,7 @@ ${this.description} } setExtensionsControlManifest(extensionsControlManifest: IExtensionsControlManifest): void { - this.isMalicious = isMalicious(this.identifier, extensionsControlManifest); + this.malicious = isMalicious(this.identifier, extensionsControlManifest.malicious); this.deprecationInfo = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[this.identifier.id.toLowerCase()] : undefined; this._extensionEnabledWithPreRelease = extensionsControlManifest?.extensionsEnabledWithPreRelease?.includes(this.identifier.id.toLowerCase()); } @@ -628,8 +631,8 @@ class Extensions extends Disposable { } } - private _local: IExtension[] | undefined; - get local(): IExtension[] { + private _local: Extension[] | undefined; + get local(): Extension[] { if (!this._local) { this._local = []; for (const extension of this.installed) { @@ -884,8 +887,8 @@ class Extensions extends Disposable { if (extension.local) { const enablementState = this.extensionEnablementService.getEnablementState(extension.local); if (enablementState !== extension.enablementState) { - (extension as Extension).enablementState = enablementState; - this._onChange.fire({ extension: extension as Extension }); + extension.enablementState = enablementState; + this._onChange.fire({ extension }); } } } @@ -940,6 +943,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IEditorService private readonly editorService: IEditorService, @IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, + @IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService, @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @INotificationService private readonly notificationService: INotificationService, @@ -1020,30 +1024,9 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension urlService.registerHandler(this); - if (this.productService.quality !== 'stable') { - this.registerAutoRestartConfig(); - } - this.whenInitialized = this.initialize(); } - private registerAutoRestartConfig(): void { - Registry.as(ConfigurationExtensions.Configuration) - .registerConfiguration({ - id: 'extensions', - order: 30, - title: nls.localize('extensionsConfigurationTitle', "Extensions"), - type: 'object', - properties: { - [AutoRestartConfigurationKey]: { - type: 'boolean', - description: nls.localize('autoRestart', "If activated, extensions will automatically restart following an update if the window is not in focus. There can be a data loss if you have open Notebooks or Custom Editors."), - default: false, - } - } - }); - } - private async initialize(): Promise { // initialize local extensions await Promise.all([this.queryLocal(), this.extensionService.whenInstalledExtensionsRegistered()]); @@ -2301,7 +2284,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (extension.gallery) { - if (!extension.gallery.isSigned) { + if (!extension.gallery.isSigned && (await this.extensionGalleryManifestService.getExtensionGalleryManifest())?.capabilities.signing?.allRepositorySigned) { return new MarkdownString().appendText(nls.localize('not signed', "This extension is not signed.")); } @@ -2333,6 +2316,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension async install(arg: string | URI | IExtension, installOptions: InstallExtensionOptions = {}, progressLocation?: ProgressLocation | string): Promise { let installable: URI | IGalleryExtension | IResourceExtension | undefined; let extension: IExtension | undefined; + let servers: IExtensionManagementServer[] | undefined; if (arg instanceof URI) { installable = arg; @@ -2379,11 +2363,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // If requested to install everywhere // then install the extension in all the servers where it is not installed if (installOptions.installEverywhere) { - installOptions.servers = []; + servers = []; const installableServers = await this.extensionManagementService.getInstallableServers(gallery); for (const extensionsServer of this.extensionsServers) { if (installableServers.includes(extensionsServer.server) && !extensionsServer.local.find(e => areSameExtensions(e.identifier, gallery.identifier))) { - installOptions.servers.push(extensionsServer.server); + servers.push(extensionsServer.server); } } } @@ -2391,17 +2375,17 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Check if the extension is disabled because of extension kind // If so, install the extension in the server that is compatible. else if (installOptions.enable && extension?.local) { - installOptions.servers = []; + servers = []; if (extension.enablementState === EnablementState.DisabledByExtensionKind) { const [installableServer] = await this.extensionManagementService.getInstallableServers(gallery); if (installableServer) { - installOptions.servers.push(installableServer); + servers.push(installableServer); } } } } - if (!installOptions.servers || installOptions.servers.length) { + if (!servers || servers.length) { if (!installable) { if (!gallery) { const id = isString(arg) ? arg : (arg).identifier.id; @@ -2458,7 +2442,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extension.resourceExtension) { extension = await this.doInstall(extension, () => this.extensionManagementService.installResourceExtension(installable as IResourceExtension, installOptions), progressLocation); } else { - extension = await this.doInstall(extension, () => this.installFromGallery(extension!, installable as IGalleryExtension, installOptions), progressLocation); + extension = await this.doInstall(extension, () => this.installFromGallery(extension!, installable as IGalleryExtension, installOptions, servers), progressLocation); } } } @@ -2762,15 +2746,15 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return this.extensionManagementService.installVSIX(vsix, manifest, installOptions); } - private installFromGallery(extension: IExtension, gallery: IGalleryExtension, installOptions?: InstallExtensionOptions): Promise { + private installFromGallery(extension: IExtension, gallery: IGalleryExtension, installOptions: InstallExtensionOptions, servers: IExtensionManagementServer[] | undefined): Promise { installOptions = installOptions ?? {}; installOptions.pinned = extension.local?.pinned || !this.shouldAutoUpdateExtension(extension); - if (extension.local && !installOptions.servers) { + if (extension.local && !servers) { installOptions.productVersion = this.getProductVersion(); installOptions.operation = InstallOperation.Update; return this.extensionManagementService.updateFromGallery(gallery, extension.local, installOptions); } else { - return this.extensionManagementService.installFromGallery(gallery, installOptions); + return this.extensionManagementService.installFromGallery(gallery, installOptions, servers); } } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 30bbba041ad..df7aaffb5e9 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -92,11 +92,17 @@ .extension-list-item > .details > .header-container > .header > .activation-status, .extension-list-item > .details > .header-container > .header > .install-count, +.extension-list-item > .details > .header-container > .header .extension-kind-indicator, .extension-list-item > .details > .header-container > .header > .ratings { display: flex; align-items: center; } +.extension-list-item > .details > .header-container > .header .extension-kind-indicator { + font-size: 80%; + margin-left: 2px; +} + .extension-list-item > .details > .header-container > .header > .install-count:not(:empty) { font-size: 80%; margin: 0 6px; @@ -179,24 +185,27 @@ align-items: center; } -.extension-list-item > .details > .footer > .author { +.extension-list-item > .details > .footer > .publisher-container { flex: 1; - display: flex; - align-items: center; line-height: 24px; } -.extension-list-item > .details > .footer > .author > .publisher-name { +.extension-list-item > .details > .footer .publisher { + display: flex; + align-items: center; +} + +.extension-list-item > .details > .footer .publisher > .publisher-name { font-size: 90%; color: var(--vscode-descriptionForeground); font-weight: 600; } -.monaco-list-row.selected .extension-list-item > .details > .footer > .author > .publisher-name{ +.monaco-list-row.selected .extension-list-item > .details > .footer .publisher > .publisher-name{ color: unset; } -.extension-list-item > .details > .footer > .author > .publisher-name:not(:first-child) { +.extension-list-item > .details > .footer .publisher > .publisher-name:not(:first-child) { padding-left: 1px; } @@ -227,7 +236,7 @@ min-width: 0; } -.monaco-list-row.disabled:not(.selected) .extension-list-item > .details > .footer > .author > .publisher-name { +.monaco-list-row.disabled:not(.selected) .extension-list-item > .details > .footer .publisher > .publisher-name { color: var(--vscode-disabledForeground); } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 03023ddf5fe..62be7a0b84a 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -148,6 +148,7 @@ } .extension-editor > .header > .details > .subtitle, +.extension-editor > .header > .details > .subtitle .extension-kind-indicator, .extension-editor > .header > .details > .subtitle .install, .extension-editor > .header > .details > .subtitle .rating, .extension-editor > .header > .details > .subtitle .sponsor { @@ -163,10 +164,14 @@ margin-left: 6px; } -.extension-editor > .header > .details > .subtitle > div:not(:first-child):not(:empty):not(.resource) { - border-left: 1px solid rgba(128, 128, 128, 0.7); - margin-left: 14px; - padding-left: 14px; +.extension-editor > .header > .details > .subtitle .extension-kind-indicator > .codicon { + margin-right: 6px; +} + +.extension-editor > .header > .details > .subtitle > .subtitle-entry:not(:empty):not(.last-non-empty) { + border-right: 1px solid rgba(128, 128, 128, 0.7); + margin-right: 14px; + padding-right: 14px; } .extension-editor > .header > .details > .description { @@ -734,7 +739,7 @@ padding-top: 5px; } -.extension-editor .subcontent .monaco-list-row .extension > .details > .footer > .author { +.extension-editor .subcontent .monaco-list-row .extension > .details > .footer .publisher { font-size: 90%; font-weight: 600; opacity: 0.6; diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css index 6a7ab84a23a..c5d81e6c952 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css @@ -31,12 +31,12 @@ opacity: .75; } -.extension-verified-publisher { +.verified-publisher { display: flex; align-items: center; } -.extension-verified-publisher > .extension-verified-publisher-domain { +.verified-publisher > .extension-verified-publisher-domain { padding-left: 2px; color: var(--vscode-extensionIcon-verifiedForeground); text-decoration: var(--text-link-decoration); diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index e8f26f634ca..4a232d2d560 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -6,8 +6,8 @@ import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { Event } from '../../../../base/common/event.js'; import { IPager } from '../../../../base/common/paging.js'; -import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, InstallExtensionResult } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { EnablementState, IExtensionManagementServer, IResourceExtension, IWorkbenchInstallOptions } from '../../../services/extensionManagement/common/extensionManagement.js'; +import { IQueryOptions, ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, InstallExtensionResult, InstallOptions } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { EnablementState, IExtensionManagementServer, IResourceExtension } from '../../../services/extensionManagement/common/extensionManagement.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; @@ -68,6 +68,7 @@ export interface IExtension { readonly publisherSponsorLink?: URI; readonly pinned: boolean; readonly version: string; + readonly private: boolean; readonly latestVersion: string; readonly preRelease: boolean; readonly isPreReleaseVersion: boolean; @@ -83,6 +84,7 @@ export interface IExtension { readonly installCount?: number; readonly rating?: number; readonly ratingCount?: number; + readonly ratingUrl?: string; readonly outdated: boolean; readonly outdatedTargetPlatform: boolean; readonly runtimeState: ExtensionRuntimeState | undefined; @@ -108,7 +110,7 @@ export interface IExtension { export const IExtensionsWorkbenchService = createDecorator('extensionsWorkbenchService'); -export interface InstallExtensionOptions extends IWorkbenchInstallOptions { +export interface InstallExtensionOptions extends InstallOptions { version?: string; justification?: string | { reason: string; action: string }; enable?: boolean; @@ -263,4 +265,5 @@ export interface IExtensionArg { id: string; version: string; location: URI | undefined; + galleryLink: string | undefined; } diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts index d7ab04b698f..e78c2df7839 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts @@ -23,7 +23,7 @@ import { DebugExtensionHostAction, DebugExtensionsContribution } from './debugEx import { ExtensionHostProfileService } from './extensionProfileService.js'; import { CleanUpExtensionsFolderAction, OpenExtensionsFolderAction } from './extensionsActions.js'; import { ExtensionsAutoProfiler } from './extensionsAutoProfiler.js'; -import { InstallFailedRemoteExtensionsContribution, RemoteExtensionsInitializerContribution } from './remoteExtensionsInit.js'; +import { InstallRemoteExtensionsContribution, RemoteExtensionsInitializerContribution } from './remoteExtensionsInit.js'; import { IExtensionHostProfileService, OpenExtensionHostProfileACtion, RuntimeExtensionsEditor, SaveExtensionHostProfileAction, StartExtensionHostProfileAction, StopExtensionHostProfileAction } from './runtimeExtensionsEditor.js'; // Singletons @@ -71,7 +71,7 @@ const workbenchRegistry = Registry.as(Workbench workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(ExtensionsAutoProfiler, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(RemoteExtensionsInitializerContribution, LifecyclePhase.Restored); -workbenchRegistry.registerWorkbenchContribution(InstallFailedRemoteExtensionsContribution, LifecyclePhase.Restored); +workbenchRegistry.registerWorkbenchContribution(InstallRemoteExtensionsContribution, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(DebugExtensionsContribution, LifecyclePhase.Restored); // Register Commands diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/remoteExtensionsInit.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/remoteExtensionsInit.ts index 9bdf631a0bc..1b0fadda14f 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/remoteExtensionsInit.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/remoteExtensionsInit.ts @@ -4,13 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT, IExtensionGalleryService, IExtensionManagementService, InstallExtensionInfo } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; +import { ExtensionType } from '../../../../platform/extensions/common/extensions.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { REMOTE_DEFAULT_IF_LOCAL_EXTENSIONS } from '../../../../platform/remote/common/remote.js'; import { IRemoteAuthorityResolverService } from '../../../../platform/remote/common/remoteAuthorityResolver.js'; import { IRemoteExtensionsScannerService } from '../../../../platform/remote/common/remoteExtensionsScanner.js'; import { IStorageService, IS_NEW_KEY, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; @@ -26,17 +30,86 @@ import { IExtensionManagementServerService } from '../../../services/extensionMa import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; -export class InstallFailedRemoteExtensionsContribution implements IWorkbenchContribution { +export class InstallRemoteExtensionsContribution implements IWorkbenchContribution { constructor( @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IRemoteExtensionsScannerService private readonly remoteExtensionsScannerService: IRemoteExtensionsScannerService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProductService private readonly productService: IProductService, ) { + this.installDefaultRemoteExtensions(); this.installFailedRemoteExtensions(); } + private async installDefaultRemoteExtensions(): Promise { + if (!this.remoteAgentService.getConnection()) { + return; + } + + if (!this.extensionManagementServerService.remoteExtensionManagementServer) { + this.logService.error('No remote extension management server available'); + return; + } + + if (!this.extensionManagementServerService.localExtensionManagementServer) { + this.logService.error('No local extension management server available'); + return; + } + + const settingValue = this.configurationService.getValue(REMOTE_DEFAULT_IF_LOCAL_EXTENSIONS); + if (!settingValue?.length) { + return; + } + + this.logService.info(`Installing '${settingValue.length}' default remote extensions`); + + const preferPrerelease = this.productService.quality !== 'stable'; + const galleryExtensions = await this.extensionGalleryService.getExtensions(settingValue.map((id) => ({ id })), CancellationToken.None); + const alreadyInstalledInRemote = await this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.getInstalled(ExtensionType.User); + const alreadyInstalledLocally = await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.getInstalled(ExtensionType.User); + + const prereleaseExtensionInfo: InstallExtensionInfo[] = []; + const extensionInfo: InstallExtensionInfo[] = []; + for (const id of settingValue) { + const alreadyInstalled = alreadyInstalledInRemote.some(e => areSameExtensions(e.identifier, { id })); + if (alreadyInstalled) { + this.logService.trace(`Default remote extension '${id}' is already installed`); + continue; + } + + const installedLocally = alreadyInstalledLocally.some(e => areSameExtensions(e.identifier, { id })); + if (!installedLocally) { + this.logService.trace(`Default remote extension '${id}' is not already installed locally`); + continue; + } + + const extension = galleryExtensions.find(e => areSameExtensions(e.identifier, { id })); + if (!extension) { + this.logService.warn(`Default remote extension '${id}' is not found`); + continue; + } + + const installPreReleaseVersion = preferPrerelease && extension.hasPreReleaseVersion; + (installPreReleaseVersion ? prereleaseExtensionInfo : extensionInfo).push({ + extension, options: { installPreReleaseVersion }, + }); + } + + // Install pre-release extensions first to avoid a situation where: + // An extension without a pre-release (A) is installed first and depends on an extension that has a pre-release version (B) + // If this happens, the extension A may result in the installation of the stable version of B + // A real life example of this is GitHub.copilot and GitHub.copilot-chat + if (prereleaseExtensionInfo.length) { + await Promise.allSettled(prereleaseExtensionInfo.map(e => this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.installFromGallery(e.extension, e.options))); + } + if (extensionInfo.length) { + await Promise.allSettled(extensionInfo.map(e => this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.installFromGallery(e.extension, e.options))); + } + } + private async installFailedRemoteExtensions(): Promise { if (!this.remoteAgentService.getConnection()) { return; diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index addaf29046f..c8c1f5e3314 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -16,7 +16,7 @@ import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; -import { IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; import { TextFileContentProvider } from '../../common/files.js'; import { FileEditorInput } from './fileEditorInput.js'; import { SAVE_FILE_AS_LABEL } from '../fileConstants.js'; @@ -45,13 +45,13 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa static readonly ID = 'workbench.contrib.textFileSaveErrorHandler'; private readonly messages = new ResourceMap(); - private readonly conflictResolutionContext = new RawContextKey(CONFLICT_RESOLUTION_CONTEXT, false, true).bindTo(this.contextKeyService); + private readonly conflictResolutionContext: IContextKey; private activeConflictResolutionResource: URI | undefined = undefined; constructor( @INotificationService private readonly notificationService: INotificationService, @ITextFileService private readonly textFileService: ITextFileService, - @IContextKeyService private contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService, @IEditorService private readonly editorService: IEditorService, @ITextModelService textModelService: ITextModelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -59,6 +59,8 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa ) { super(); + this.conflictResolutionContext = new RawContextKey(CONFLICT_RESOLUTION_CONTEXT, false, true).bindTo(contextKeyService); + const provider = this._register(instantiationService.createInstance(TextFileContentProvider)); this._register(textModelService.registerTextModelContentProvider(CONFLICT_RESOLUTION_SCHEME, provider)); diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index cb60e6d4a34..61f0ac0a8ff 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -26,7 +26,6 @@ import { IHostService } from '../../../services/host/browser/host.js'; import { IExpression } from '../../../../base/common/glob.js'; import { ResourceGlobMatcher } from '../../../common/resources.js'; import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; export const UNDO_REDO_SOURCE = new UndoRedoSource(); @@ -55,8 +54,7 @@ export class ExplorerService implements IExplorerService { @IBulkEditService private readonly bulkEditService: IBulkEditService, @IProgressService private readonly progressService: IProgressService, @IHostService hostService: IHostService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService ) { this.config = this.configurationService.getValue('explorer'); @@ -245,46 +243,6 @@ export class ExplorerService implements IExplorerService { try { await this.view.setEditable(stat, isEditing); } catch { - const parent = stat.parent; - type ExplorerViewEditableErrorData = { - parentIsDirectory: boolean | undefined; - isDirectory: boolean | undefined; - isReadonly: boolean | undefined; - parentIsReadonly: boolean | undefined; - parentIsExcluded: boolean | undefined; - isExcluded: boolean | undefined; - parentIsRoot: boolean | undefined; - isRoot: boolean | undefined; - parentHasNests: boolean | undefined; - hasNests: boolean | undefined; - }; - type ExplorerViewEditableErrorClassification = { - owner: 'lramos15'; - comment: 'Helps gain a broard understanding of why users are unable to edit files in the explorer'; - parentIsDirectory: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the parent of the editable element is a directory' }; - isDirectory: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the editable element is a directory' }; - isReadonly: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the editable element is readonly' }; - parentIsReadonly: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the parent of the editable element is readonly' }; - parentIsExcluded: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the parent of the editable element is excluded from being shown in the explorer' }; - isExcluded: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the editable element is excluded from being shown in the explorer' }; - parentIsRoot: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the parent of the editable element is a root' }; - isRoot: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the editable element is a root' }; - parentHasNests: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the parent of the editable element has nested children' }; - hasNests: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the editable element has nested children' }; - }; - const errorData = { - parentIsDirectory: parent?.isDirectory, - isDirectory: stat.isDirectory, - isReadonly: !!stat.isReadonly, - parentIsReadonly: !!parent?.isReadonly, - parentIsExcluded: parent?.isExcluded, - isExcluded: stat.isExcluded, - parentIsRoot: parent?.isRoot, - isRoot: stat.isRoot, - parentHasNests: parent?.hasNests, - hasNests: stat.hasNests, - }; - this.telemetryService.publicLogError2('explorerView.setEditableError', errorData); return; } diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index a80963b7f62..2eec08fd35a 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -14,7 +14,6 @@ import { CancellationToken, CancellationTokenSource } from '../../../../base/com import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { formatDocumentRangesWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered, FormattingConflicts, FormattingMode, FormattingKind } from '../../../../editor/contrib/format/browser/format.js'; import { Range } from '../../../../editor/common/core/range.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; @@ -263,28 +262,6 @@ interface IIndexedPick extends IQuickPickItem { index: number; } -function logFormatterTelemetry(telemetryService: ITelemetryService, mode: 'document' | 'range', options: T[], pick?: T) { - type FormatterPicks = { - mode: 'document' | 'range'; - extensions: string[]; - pick: string; - }; - type FormatterPicksClassification = { - owner: 'jrieken'; - comment: 'Information about resolving formatter conflicts'; - mode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Formatting mode: whole document or a range/selection' }; - extensions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension that got picked' }; - pick: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The possible extensions to pick' }; - }; - function extKey(obj: T): string { - return obj.extensionId ? ExtensionIdentifier.toKey(obj.extensionId) : 'unknown'; - } - telemetryService.publicLog2('formatterpick', { - mode, - extensions: options.map(extKey), - pick: pick ? extKey(pick) : 'none' - }); -} async function showFormatterPick(accessor: ServicesAccessor, model: ITextModel, formatters: FormattingEditProvider[]): Promise { const quickPickService = accessor.get(IQuickInputService); @@ -362,7 +339,6 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { return; } const instaService = accessor.get(IInstantiationService); - const telemetryService = accessor.get(ITelemetryService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); const model = editor.getModel(); const provider = getRealAndSyntheticDocumentFormattersOrdered(languageFeaturesService.documentFormattingEditProvider, languageFeaturesService.documentRangeFormattingEditProvider, model); @@ -370,7 +346,6 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { if (typeof pick === 'number') { await instaService.invokeFunction(formatDocumentWithProvider, provider[pick], editor, FormattingMode.Explicit, CancellationToken.None); } - logFormatterTelemetry(telemetryService, 'document', provider, typeof pick === 'number' && provider[pick] || undefined); } }); @@ -396,7 +371,6 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction { } const instaService = accessor.get(IInstantiationService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); - const telemetryService = accessor.get(ITelemetryService); const model = editor.getModel(); let range: Range = editor.getSelection(); @@ -409,7 +383,5 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction { if (typeof pick === 'number') { await instaService.invokeFunction(formatDocumentRangesWithProvider, provider[pick], editor, range, CancellationToken.None, true); } - - logFormatterTelemetry(telemetryService, 'range', provider, typeof pick === 'number' && provider[pick] || undefined); } }); diff --git a/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts index 0494623f078..ab3d1dabf53 100644 --- a/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts +++ b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts @@ -174,7 +174,7 @@ registerAction2(class StartReadHints extends EditorAction2 { constructor() { super({ id: 'inlayHints.startReadingLineWithHint', - title: localize2('read.title', "Read Line With Inline Hints"), + title: localize2('read.title', "Read Line with Inline Hints"), precondition: EditorContextKeys.hasInlayHintsProvider, f1: true }); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index b23098134a1..434bffb469d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatController1, InlineChatController2, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_HAS_AGENT2 } from '../common/inlineChat.js'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_HAS_AGENT2, MENU_INLINE_CHAT_SIDE } from '../common/inlineChat.js'; import { ctxIsGlobalEditingSession, ctxRequestCount } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; @@ -70,8 +70,8 @@ export class StartSessionAction extends Action2 { icon: START_INLINE_CHAT, menu: { id: MenuId.ChatTitleBarMenu, - group: 'd_inlineChat', - order: 10, + group: 'a_open', + order: 3, } }); } @@ -577,20 +577,26 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { } export class StopSessionAction2 extends AbstractInline2ChatAction { + constructor() { super({ id: 'inlineChat2.stop', - title: localize2('stop', "Stop"), + title: localize2('stop', "Undo & Close"), f1: true, + icon: Codicon.close, precondition: CTX_INLINE_CHAT_VISIBLE, keybinding: [{ - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyCode.Escape, - }, { when: ctxRequestCount.isEqualTo(0), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KeyI, + }, { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape, }], + menu: { + id: MENU_INLINE_CHAT_SIDE, + group: 'navigation', + } }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 55da3ca89eb..e9181c51ce0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -12,7 +12,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { MovingAverage } from '../../../../base/common/numbers.js'; -import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { assertType } from '../../../../base/common/types.js'; @@ -41,9 +41,6 @@ import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/edit import { IViewsService } from '../../../services/views/common/viewsService.js'; import { showChatView } from '../../chat/browser/chat.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; -import { ChatAgentLocation } from '../../chat/common/chatAgents.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; -import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js'; import { IChatService } from '../../chat/common/chatService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; @@ -54,6 +51,9 @@ import { InlineChatError } from './inlineChatSessionServiceImpl.js'; import { HunkAction, IEditObserver, LiveStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; +import { ChatAgentLocation } from '../../chat/common/constants.js'; +import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -1450,14 +1450,17 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito chatRequest.response.complete(); } - const whenDecided = new Promise(resolve => { - store.add(autorun(r => { - if (!editSession.entries.read(r).some(e => e.state.read(r) === WorkingSetEntryState.Modified)) { - resolve(undefined); - } - })); + const isSettled = derived(r => { + const entry = editSession.readEntry(uri, r); + if (!entry) { + return false; + } + const state = entry.state.read(r); + return state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected; }); + const whenDecided = waitForState(isSettled, Boolean); + await raceCancellation(whenDecided, token); store.dispose(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts index e4143ceff4a..0e6b7f98cee 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts @@ -20,7 +20,7 @@ import { IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/ import { URI } from '../../../../base/common/uri.js'; import { isEqual } from '../../../../base/common/resources.js'; import { StandardTokenType } from '../../../../editor/common/encodedTokenAttributes.js'; -import { autorun, derived, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { autorun, derivedWithStore, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import './media/inlineChat.css'; @@ -38,6 +38,7 @@ import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modes import { createStyleSheet2 } from '../../../../base/browser/domStylesheets.js'; import { stringValue } from '../../../../base/browser/cssValue.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { Emitter } from '../../../../base/common/event.js'; export const CTX_INLINE_CHAT_SHOWING_HINT = new RawContextKey('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint")); @@ -222,7 +223,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont const configHintEmpty = observableConfigValue(InlineChatConfigKeys.LineEmptyHint, false, this._configurationService); const configHintNL = observableConfigValue(InlineChatConfigKeys.LineNLHint, false, this._configurationService); - const showDataObs = derived(r => { + const showDataObs = derivedWithStore((r, store) => { const ghostState = ghostCtrl?.model.read(r)?.state.read(r); const textFocus = editorObs.isTextFocused.read(r); @@ -239,6 +240,12 @@ export class InlineChatHintsController extends Disposable implements IEditorCont return undefined; } + // DEBT - I cannot use `model.onDidChangeContent` directly here + // https://github.com/microsoft/vscode/issues/242059 + const emitter = store.add(new Emitter()); + store.add(model.onDidChangeContent(() => emitter.fire())); + observableFromEvent(emitter.event, () => model.getVersionId()).read(r); + const visible = this._visibilityObs.read(r); const isEol = model.getLineMaxColumn(position.lineNumber) === position.column; const isWhitespace = model.getLineLastNonWhitespaceColumn(position.lineNumber) === 0 && model.getValueLength() > 0 && position.column > 1; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 5f06c2dacb4..7765688673b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -34,6 +34,7 @@ import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/cha import { assertType } from '../../../../base/common/types.js'; import { autorun } from '../../../../base/common/observable.js'; import { ResourceMap } from '../../../../base/common/map.js'; +import { IChatWidgetService } from '../../chat/browser/chat.js'; type SessionData = { @@ -85,6 +86,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { @IChatService private readonly _chatService: IChatService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, ) { } dispose() { @@ -338,7 +340,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const chatModel = this._chatService.startSession(ChatAgentLocation.EditingSession, token); const editingSession = await this._chatEditingService.createEditingSession(chatModel.sessionId); - editingSession.addFileToWorkingSet(uri); + const widget = this._chatWidgetService.getWidgetBySessionId(chatModel.sessionId); + widget?.attachmentModel.addFile(uri); const store = new DisposableStore(); store.add(toDisposable(() => { @@ -406,10 +409,10 @@ export class InlineChatEnabler { const updateAgent = () => { const agent = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor); - if (agent?.locations.length === 1) { + if (agent?.id === 'github.copilot.editor') { this._ctxHasProvider.set(true); this._ctxHasProvider2.reset(); - } else if (agent?.locations.includes(ChatAgentLocation.EditingSession)) { + } else if (agent?.id === 'github.copilot.editingSessionEditor') { this._ctxHasProvider.reset(); this._ctxHasProvider2.set(true); } else { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 138b51910c4..83836042939 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -22,7 +22,7 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IChatWidgetViewOptions } from '../../chat/browser/chat.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; import { isResponseVM } from '../../chat/common/chatViewModel.js'; -import { ACTION_REGENERATE_RESPONSE, ACTION_REPORT_ISSUE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_SECONDARY, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; +import { ACTION_REGENERATE_RESPONSE, ACTION_REPORT_ISSUE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_SIDE, MENU_INLINE_CHAT_WIDGET_SECONDARY, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; export class InlineChatZoneWidget extends ZoneWidget { @@ -81,6 +81,7 @@ export class InlineChatZoneWidget extends ZoneWidget { chatWidgetViewOptions: { menus: { telemetrySource: 'interactiveEditorWidget-toolbar', + inputSideToolbar: MENU_INLINE_CHAT_SIDE }, ...options, rendererOptions: { diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index d2a96c1d2cd..3b6979b8f10 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -106,6 +106,8 @@ export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.statu export const MENU_INLINE_CHAT_WIDGET_SECONDARY = MenuId.for('inlineChatWidget.secondary'); export const MENU_INLINE_CHAT_ZONE = MenuId.for('inlineChatWidget.changesZone'); +export const MENU_INLINE_CHAT_SIDE = MenuId.for('inlineChatWidget.side'); + // --- colors diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index f2d8cc86365..5e2024222e7 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -49,7 +49,7 @@ import { IChatVariablesService } from '../../../chat/common/chatVariables.js'; import { IChatWidgetHistoryService, ChatWidgetHistoryService } from '../../../chat/common/chatWidgetHistoryService.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { TestExtensionService, TestContextService } from '../../../../test/common/workbenchTestServices.js'; -import { IChatAgentService, ChatAgentService, ChatAgentLocation } from '../../../chat/common/chatAgents.js'; +import { IChatAgentService, ChatAgentService } from '../../../chat/common/chatAgents.js'; import { ChatVariablesService } from '../../../chat/browser/chatVariables.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { TestCommandService } from '../../../../../editor/test/browser/editorTestServices.js'; @@ -62,6 +62,7 @@ import { IChatRequestModel } from '../../../chat/common/chatModel.js'; import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; import { IObservable, constObservable } from '../../../../../base/common/observable.js'; import { IChatEditingService, IChatEditingSession } from '../../../chat/common/chatEditingService.js'; +import { ChatAgentLocation } from '../../../chat/common/constants.js'; suite('InlineChatSession', function () { diff --git a/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts b/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts index 8071173c048..75930b380dd 100644 --- a/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts +++ b/src/vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts @@ -5,24 +5,29 @@ import { localize } from '../../../../nls.js'; import { createHotClass } from '../../../../base/common/hotReloadHelpers.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { autorunWithStore, derived } from '../../../../base/common/observable.js'; import { debouncedObservable } from '../../../../base/common/observableInternal/utils.js'; import Severity from '../../../../base/common/severity.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; import { ILanguageStatusService } from '../../../services/languageStatus/common/languageStatusService.js'; +import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; export class InlineCompletionLanguageStatusBarContribution extends Disposable { public static readonly hot = createHotClass(InlineCompletionLanguageStatusBarContribution); public static Id = 'vs.editor.contrib.inlineCompletionLanguageStatusBarContribution'; + public static readonly languageStatusBarDisposables = new Set(); private readonly _c = InlineCompletionsController.get(this._editor); private readonly _state = derived(this, reader => { const model = this._c?.model.read(reader); if (!model) { return undefined; } + if (!observableCodeEditor(this._editor).isFocused.read(reader)) { + return undefined; + } return { model, @@ -51,11 +56,20 @@ export class InlineCompletionLanguageStatusBarContribution extends Disposable { noSuggestion: { shortLabel: '$(circle-slash)', label: '$(copilot) ' + localize('noInlineSuggestionAvailable', "No inline suggestion available"), loading: false, }, }; + // Make sure previous status is cleared before the new is registered. This works, but is a bit hacky. + // TODO: Use a workbench contribution to get singleton behavior. + InlineCompletionLanguageStatusBarContribution.languageStatusBarDisposables.forEach(d => d.clear()); + + InlineCompletionLanguageStatusBarContribution.languageStatusBarDisposables.add(store); + store.add({ + dispose: () => InlineCompletionLanguageStatusBarContribution.languageStatusBarDisposables.delete(store) + }); + store.add(this._languageStatusService.addStatus({ accessibilityInfo: undefined, busy: statusMap[status].loading, command: undefined, - detail: localize('inlineSuggestions', "Inline Suggestions"), + detail: localize('inlineSuggestionsSmall', "Inline suggestions"), id: 'inlineSuggestions', label: { value: statusMap[status].label, shortValue: statusMap[status].shortLabel }, name: localize('inlineSuggestions', "Inline Suggestions"), diff --git a/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts index 38da042c409..c5074ca006a 100644 --- a/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts +++ b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts @@ -65,6 +65,8 @@ export class BaseIssueReporterService extends Disposable { public delayedSubmit = new Delayer(300); public previewButton!: Button; public nonGitHubIssueUrl = false; + public needsUpdate = false; + public acknowledged = false; constructor( public disableExtensions: boolean, @@ -103,7 +105,7 @@ export class BaseIssueReporterService extends Disposable { //TODO: Handle case where extension is not activated const issueReporterElement = this.getElementById('issue-reporter'); if (issueReporterElement) { - this.previewButton = new Button(issueReporterElement, unthemedButtonStyles); + this.previewButton = this._register(new Button(issueReporterElement, unthemedButtonStyles)); const issueRepoName = document.createElement('a'); issueReporterElement.appendChild(issueRepoName); issueRepoName.id = 'show-repo-name'; @@ -141,7 +143,7 @@ export class BaseIssueReporterService extends Disposable { } const delayer = new RunOnceScheduler(updateAll, 0); - iconsStyleSheet.onDidChange(() => delayer.schedule()); + this._register(iconsStyleSheet.onDidChange(() => delayer.schedule())); delayer.schedule(); this.handleExtensionData(data.enabledExtensions); @@ -387,6 +389,14 @@ export class BaseIssueReporterService extends Disposable { } } + private updateAcknowledgementState() { + const acknowledgementCheckbox = this.getElementById('includeAcknowledgement'); + if (acknowledgementCheckbox) { + this.acknowledged = acknowledgementCheckbox.checked; + this.updatePreviewButtonState(); + } + } + public setEventHandlers(): void { (['includeSystemInfo', 'includeProcessInfo', 'includeWorkspaceInfo', 'includeExtensions', 'includeExperiments', 'includeExtensionData'] as const).forEach(elementId => { this.addEventListener(elementId, 'click', (event: Event) => { @@ -395,6 +405,11 @@ export class BaseIssueReporterService extends Disposable { }); }); + this.addEventListener('includeAcknowledgement', 'click', (event: Event) => { + event.stopPropagation(); + this.updateAcknowledgementState(); + }); + const showInfoElements = this.window.document.getElementsByClassName('showInfo'); for (let i = 0; i < showInfoElements.length; i++) { const showInfo = showInfoElements.item(i)!; @@ -490,11 +505,11 @@ export class BaseIssueReporterService extends Disposable { this.searchIssues(title, fileOnExtension, fileOnMarketplace); }); - this.previewButton.onDidClick(async () => { + this._register(this.previewButton.onDidClick(async () => { this.delayedSubmit.trigger(async () => { this.createIssue(); }); - }); + })); this.addEventListener('disableExtensions', 'click', () => { this.issueFormService.reloadWithExtensionsDisabled(); @@ -561,7 +576,11 @@ export class BaseIssueReporterService extends Disposable { } public updatePreviewButtonState() { - if (this.isPreviewEnabled()) { + + if (!this.acknowledged && this.needsUpdate) { + this.previewButton.label = localize('acknowledge', "Confirm Version Acknowledgement"); + this.previewButton.enabled = false; + } else if (this.isPreviewEnabled()) { if (this.data.githubAccessToken) { this.previewButton.label = localize('createOnGitHub', "Create on GitHub"); } else { diff --git a/src/vs/workbench/contrib/issue/browser/issueFormService.ts b/src/vs/workbench/contrib/issue/browser/issueFormService.ts index bf42337b69e..c587880a09c 100644 --- a/src/vs/workbench/contrib/issue/browser/issueFormService.ts +++ b/src/vs/workbench/contrib/issue/browser/issueFormService.ts @@ -98,11 +98,13 @@ export class IssueFormService implements IIssueFormService { this.issueReporterWindow = auxiliaryWindow.window; } else { console.error('Failed to open auxiliary window'); + disposables.dispose(); } // handle closing issue reporter this.issueReporterWindow?.addEventListener('beforeunload', () => { auxiliaryWindow.window.close(); + disposables.dispose(); this.issueReporterWindow = null; }); } diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts b/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts index fbd7fbb3fb8..e739f44817f 100644 --- a/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts @@ -12,6 +12,7 @@ const sendWorkspaceInfoLabel = escape(localize('sendWorkspaceInfo', "Include my const sendExtensionsLabel = escape(localize('sendExtensions', "Include my enabled extensions")); const sendExperimentsLabel = escape(localize('sendExperiments', "Include A/B experiment info")); const sendExtensionData = escape(localize('sendExtensionData', "Include additional extension info")); +const acknowledgementsLabel = escape(localize('acknowledgements', "I acknowledge that my VS Code version is not updated and this issue may be closed.")); const reviewGuidanceLabel = localize( // intentionally not escaped because of its embedded tags { key: 'reviewGuidanceLabel', @@ -20,10 +21,15 @@ const reviewGuidanceLabel = localize( // intentionally not escaped because of it '{Locked=""}' ] }, - 'Before you report an issue here please review the guidance we provide.' + 'Before you report an issue here please review the guidance we provide. Please complete the form in English.' ); export default (): string => ` +
@@ -154,5 +160,11 @@ export default (): string => `
+ `; diff --git a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css index 14ee09bebd1..23f3be9ab74 100644 --- a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css +++ b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css @@ -97,7 +97,6 @@ line-height: 1.5; color: #495057; background-color: #fff; - border: none; } .issue-reporter * { @@ -184,7 +183,7 @@ .issue-reporter body { margin: 0; - overflow-y: scroll; + overflow-y: auto; height: 100%; } @@ -213,12 +212,11 @@ padding-bottom: 2em; display: flex; flex-direction: column; - min-height: 100%; overflow: visible; } .issue-reporter .description-section { - flex-grow: 1; + flex-grow: 0; display: flex; flex-direction: column; flex-shrink: 0; @@ -226,12 +224,13 @@ .issue-reporter textarea { flex-grow: 1; - min-height: 150px; + height: 200px; } .issue-reporter .block-info-text { display: flex; - flex-grow: 1; + flex-grow: 0; + flex-direction: column; } .issue-reporter #github-submit-btn { @@ -320,7 +319,6 @@ .issue-reporter select, .issue-reporter textarea { background-color: #3c3c3c; - border: none; color: #cccccc; } @@ -456,3 +454,4 @@ .issue-reporter a { color: var(--vscode-textLink-foreground); } + diff --git a/src/vs/workbench/contrib/issue/common/issue.contribution.ts b/src/vs/workbench/contrib/issue/common/issue.contribution.ts index a2805613d96..43be68be59e 100644 --- a/src/vs/workbench/contrib/issue/common/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/common/issue.contribution.ts @@ -3,16 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Disposable } from '../../../../base/common/lifecycle.js'; import { localize, localize2 } from '../../../../nls.js'; import { ICommandAction } from '../../../../platform/action/common/action.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { CommandsRegistry, ICommandMetadata } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IWorkbenchIssueService, IssueReporterData } from './issue.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IssueReporterData, IWorkbenchIssueService } from './issue.js'; const OpenIssueReporterActionId = 'workbench.action.openIssueReporter'; const OpenIssueReporterApiId = 'vscode.openIssueReporter'; @@ -65,6 +66,18 @@ export class BaseIssueContribution extends Disposable implements IWorkbenchContr ) { super(); + if (configurationService.getValue('telemetry.disableFeedback')) { + this._register(CommandsRegistry.registerCommand({ + id: 'workbench.action.openIssueReporter', + handler: function (accessor) { + const data = accessor.get(INotificationService); + data.info('Feedback is disabled.'); + + }, + })); + return; + } + if (!productService.reportIssueUrl) { return; } diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts index 4ff3e642e27..9d0bcff4959 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts @@ -3,25 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IDisposable } from '../../../../base/common/lifecycle.js'; import { localize, localize2 } from '../../../../nls.js'; -import { registerAction2, Action2 } from '../../../../platform/actions/common/actions.js'; -import { IWorkbenchIssueService, IssueType, IIssueFormService } from '../common/issue.js'; -import { BaseIssueContribution } from '../common/issue.contribution.js'; +import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from '../../../../platform/quickinput/common/quickAccess.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Extensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; -import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from '../../../../platform/quickinput/common/quickAccess.js'; import { IssueQuickAccess } from '../browser/issueQuickAccess.js'; -import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; -import { NativeIssueService } from './issueService.js'; -import './processMainService.js'; import '../browser/issueTroubleshoot.js'; +import { BaseIssueContribution } from '../common/issue.contribution.js'; +import { IIssueFormService, IWorkbenchIssueService, IssueType } from '../common/issue.js'; +import { NativeIssueService } from './issueService.js'; import { NativeIssueFormService } from './nativeIssueFormService.js'; +import './processMainService.js'; //#region Issue Contribution registerSingleton(IWorkbenchIssueService, NativeIssueService, InstantiationType.Delayed); @@ -35,6 +35,10 @@ class NativeIssueContribution extends BaseIssueContribution { ) { super(productService, configurationService); + if (configurationService.getValue('telemetry.disableFeedback')) { + return; + } + if (productService.reportIssueUrl) { this._register(registerAction2(ReportPerformanceIssueUsingReporterAction)); } diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts index e6be6f94d32..bb27e35e45d 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts @@ -16,6 +16,7 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; import { IProcessMainService } from '../../../../platform/process/common/process.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; import { applyZoom } from '../../../../platform/window/electron-sandbox/window.js'; import { BaseIssueReporterService } from '../browser/baseIssueReporterService.js'; import { IssueReporterData as IssueReporterModelData } from '../browser/issueReporterModel.js'; @@ -47,7 +48,8 @@ export class IssueReporter extends BaseIssueReporterService { @IProcessMainService processMainService: IProcessMainService, @IThemeService themeService: IThemeService, @IFileService fileService: IFileService, - @IFileDialogService fileDialogService: IFileDialogService + @IFileDialogService fileDialogService: IFileDialogService, + @IUpdateService private readonly updateService: IUpdateService ) { super(disableExtensions, data, os, product, window, false, issueFormService, themeService, fileService, fileDialogService); this.processMainService = processMainService; @@ -64,6 +66,7 @@ export class IssueReporter extends BaseIssueReporterService { }); } + this.checkForUpdates(); this.setEventHandlers(); applyZoom(this.data.zoomLevel, this.window); this.updateExperimentsInfo(this.data.experiments); @@ -71,6 +74,20 @@ export class IssueReporter extends BaseIssueReporterService { this.updateUnsupportedMode(this.data.isUnsupported); } + private async checkForUpdates(): Promise { + const updateState = this.updateService.state; + if (updateState.type === StateType.Ready || updateState.type === StateType.Downloaded) { + this.needsUpdate = true; + const includeAcknowledgement = this.getElementById('version-acknowledgements'); + const updateBanner = this.getElementById('update-banner'); + if (updateBanner && includeAcknowledgement) { + includeAcknowledgement.classList.remove('hidden'); + updateBanner.classList.remove('hidden'); + updateBanner.textContent = localize('updateAvailable', "A new version of {0} is available.", this.product.nameLong); + } + } + } + public override setEventHandlers(): void { super.setEventHandlers(); diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css b/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css index c88a95a95e0..aebbe980428 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css +++ b/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css @@ -175,7 +175,7 @@ body.issue-reporter-body { margin: 0 !important; - overflow-y: scroll !important; + overflow-y: auto !important; height: 100% !important; } @@ -204,25 +204,25 @@ body.issue-reporter-body { padding-bottom: 2em; display: flex; flex-direction: column; - min-height: 100%; overflow: visible; } .issue-reporter-body .description-section { - flex-grow: 1; + flex-grow: 0; display: flex; flex-direction: column; flex-shrink: 0; } .issue-reporter-body textarea { - flex-grow: 1; - min-height: 150px; + flex-grow: 0; + height: 200px; } .issue-reporter-body .block-info-text { display: flex; - flex-grow: 1; + flex-grow: 0; + flex-direction: column; } .issue-reporter-body #github-submit-btn { @@ -468,3 +468,13 @@ body.issue-reporter-body { .issue-reporter-body .issues-container > .issue .issue-icon { padding-top: 2px; } + +.issue-reporter-update-banner { + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + padding: 10px; + text-align: center; + position: sticky; + top: 0; + z-index: 1000; +} diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts index c202234088a..f6ae15199b8 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/issueReporter.css'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { IMenuService } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; @@ -12,13 +12,16 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; import product from '../../../../platform/product/common/product.js'; +import { IAuxiliaryWindowService } from '../../../services/auxiliaryWindow/browser/auxiliaryWindowService.js'; +import { IHostService } from '../../../services/host/browser/host.js'; import { IssueFormService } from '../browser/issueFormService.js'; import { IIssueFormService, IssueReporterData } from '../common/issue.js'; import { IssueReporter } from './issueReporterService.js'; -import { IAuxiliaryWindowService } from '../../../services/auxiliaryWindow/browser/auxiliaryWindowService.js'; -import { IHostService } from '../../../services/host/browser/host.js'; +import './media/issueReporter.css'; export class NativeIssueFormService extends IssueFormService implements IIssueFormService { + private readonly store = new DisposableStore(); + constructor( @IInstantiationService instantiationService: IInstantiationService, @IAuxiliaryWindowService auxiliaryWindowService: IAuxiliaryWindowService, @@ -53,8 +56,10 @@ export class NativeIssueFormService extends IssueFormService implements IIssueFo // create issue reporter and instantiate if (this.issueReporterWindow) { - const issueReporter = this.instantiationService.createInstance(IssueReporter, !!this.environmentService.disableExtensions, data, { type: this.type, arch: this.arch, release: this.release }, product, this.issueReporterWindow); + const issueReporter = this.store.add(this.instantiationService.createInstance(IssueReporter, !!this.environmentService.disableExtensions, data, { type: this.type, arch: this.arch, release: this.release }, product, this.issueReporterWindow)); issueReporter.render(); + } else { + this.store.dispose(); } } } diff --git a/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts index d32ed42432f..729a6932d6e 100644 --- a/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts +++ b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts @@ -103,7 +103,7 @@ class LanguageDetectionStatusContribution implements IWorkbenchContribution { text: '$(lightbulb-autofix)', }; if (!this._combinedEntry) { - this._combinedEntry = this._statusBarService.addEntry(props, LanguageDetectionStatusContribution._id, StatusbarAlignment.RIGHT, { id: 'status.editor.mode', alignment: StatusbarAlignment.RIGHT, compact: true }); + this._combinedEntry = this._statusBarService.addEntry(props, LanguageDetectionStatusContribution._id, StatusbarAlignment.RIGHT, { location: { id: 'status.editor.mode', priority: 100.1 }, alignment: StatusbarAlignment.RIGHT, compact: true }); } else { this._combinedEntry.update(props); } diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts index 142d518e68b..17bff692d74 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.ts @@ -224,7 +224,7 @@ class LanguageStatus { text: isOneBusy ? '$(loading~spin)' : text, }; if (!this._combinedEntry) { - this._combinedEntry = this._statusBarService.addEntry(props, LanguageStatus._id, StatusbarAlignment.RIGHT, { id: 'status.editor.mode', alignment: StatusbarAlignment.LEFT, compact: true }); + this._combinedEntry = this._statusBarService.addEntry(props, LanguageStatus._id, StatusbarAlignment.RIGHT, { location: { id: 'status.editor.mode', priority: 100.1 }, alignment: StatusbarAlignment.LEFT, compact: true }); } else { this._combinedEntry.update(props); } @@ -274,7 +274,7 @@ class LanguageStatus { const props = LanguageStatus._asStatusbarEntry(status); let entry = this._dedicatedEntries.get(status.id); if (!entry) { - entry = this._statusBarService.addEntry(props, status.id, StatusbarAlignment.RIGHT, { id: 'status.editor.mode', alignment: StatusbarAlignment.RIGHT }); + entry = this._statusBarService.addEntry(props, status.id, StatusbarAlignment.RIGHT, { location: { id: 'status.editor.mode', priority: 100.1 }, alignment: StatusbarAlignment.RIGHT }); } else { entry.update(props); this._dedicatedEntries.delete(status.id); diff --git a/src/vs/workbench/contrib/limitIndicator/browser/limitIndicator.contribution.ts b/src/vs/workbench/contrib/limitIndicator/browser/limitIndicator.contribution.ts index 538e67d5f0f..0640e07026d 100644 --- a/src/vs/workbench/contrib/limitIndicator/browser/limitIndicator.contribution.ts +++ b/src/vs/workbench/contrib/limitIndicator/browser/limitIndicator.contribution.ts @@ -75,7 +75,7 @@ interface LanguageFeatureAccessor { class ColorDecorationAccessor implements LanguageFeatureAccessor { readonly id = 'decoratorsLimitInfo'; readonly name = nls.localize('colorDecoratorsStatusItem.name', 'Color Decorator Status'); - readonly label = nls.localize('status.limitedColorDecorators.short', 'Color Decorators'); + readonly label = nls.localize('status.limitedColorDecorators.short', 'Color decorators'); readonly source = nls.localize('colorDecoratorsStatusItem.source', 'Color Decorators'); readonly settingsId = 'editor.colorDecoratorsLimit'; @@ -87,7 +87,7 @@ class ColorDecorationAccessor implements LanguageFeatureAccessor { class FoldingRangeAccessor implements LanguageFeatureAccessor { readonly id = 'foldingLimitInfo'; readonly name = nls.localize('foldingRangesStatusItem.name', 'Folding Status'); - readonly label = nls.localize('status.limitedFoldingRanges.short', 'Folding Ranges'); + readonly label = nls.localize('status.limitedFoldingRanges.short', 'Folding ranges'); readonly source = nls.localize('foldingRangesStatusItem.source', 'Folding'); readonly settingsId = 'editor.foldingMaximumRegions'; diff --git a/src/vs/workbench/contrib/markers/browser/markersTable.ts b/src/vs/workbench/contrib/markers/browser/markersTable.ts index af8e6eafa18..3a10bafb327 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTable.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTable.ts @@ -13,7 +13,7 @@ import { IOpenEvent, IWorkbenchTableOptions, WorkbenchTable } from '../../../../ import { HighlightedLabel } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; import { compareMarkersByUri, Marker, MarkerTableItem, ResourceMarkers } from './markersModel.js'; import { MarkerSeverity } from '../../../../platform/markers/common/markers.js'; -import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { FilterOptions } from './markersFilterOptions.js'; diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 6e639a98c13..4ed27e35584 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -26,7 +26,7 @@ import { Event, Emitter } from '../../../../base/common/event.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { isUndefinedOrNull } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import { Action, IAction } from '../../../../base/common/actions.js'; +import { Action, IAction, toAction } from '../../../../base/common/actions.js'; import { localize } from '../../../../nls.js'; import { CancelablePromise, createCancelablePromise, Delayer } from '../../../../base/common/async.js'; import { IModelService } from '../../../../editor/common/services/model.js'; @@ -35,7 +35,7 @@ import { applyCodeAction, ApplyCodeActionReason, getCodeActions } from '../../.. import { CodeActionKind, CodeActionSet, CodeActionTriggerSource } from '../../../../editor/contrib/codeAction/common/types.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { IEditorService, ACTIVE_GROUP } from '../../../services/editor/common/editorService.js'; -import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { CodeActionTriggerType } from '../../../../editor/common/languages.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { Progress } from '../../../../platform/progress/common/progress.js'; @@ -645,15 +645,14 @@ export class MarkerViewModel extends Disposable { } private toActions(codeActions: CodeActionSet): IAction[] { - return codeActions.validActions.map(item => new Action( - item.action.command ? item.action.command.id : item.action.title, - item.action.title, - undefined, - true, - () => { - return this.openFileAtMarker(this.marker) - .then(() => this.instantiationService.invokeFunction(applyCodeAction, item, ApplyCodeActionReason.FromProblemsView)); - })); + return codeActions.validActions.map(item => toAction({ + id: item.action.command ? item.action.command.id : item.action.title, + label: item.action.title, + run: async () => { + await this.openFileAtMarker(this.marker); + return await this.instantiationService.invokeFunction(applyCodeAction, item, ApplyCodeActionReason.FromProblemsView); + } + })); } private openFileAtMarker(element: Marker): Promise { diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts new file mode 100644 index 00000000000..f886d782765 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import * as jsonContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { mcpSchemaId } from '../../../services/configuration/common/configuration.js'; +import { ConfigMcpDiscovery } from '../common/discovery/configMcpDiscovery.js'; +import { ExtensionMcpDiscovery } from '../common/discovery/extensionMcpDiscovery.js'; +import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js'; +import { RemoteNativeMpcDiscovery } from '../common/discovery/nativeMcpRemoteDiscovery.js'; +import { mcpServerSchema } from '../common/mcpConfiguration.js'; +import { McpContextKeysController } from '../common/mcpContextKeys.js'; +import { McpRegistry } from '../common/mcpRegistry.js'; +import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; +import { McpService } from '../common/mcpService.js'; +import { IMcpService } from '../common/mcpTypes.js'; +import { AddConfigurationAction, ListMcpServerCommand, MCPServerActionRendering, McpServerOptionsCommand, ResetMcpCachedTools, ResetMcpTrustCommand } from './mcpCommands.js'; +import { McpDiscovery } from './mcpDiscovery.js'; + +registerSingleton(IMcpRegistry, McpRegistry, InstantiationType.Delayed); +registerSingleton(IMcpService, McpService, InstantiationType.Delayed); + +mcpDiscoveryRegistry.register(new SyncDescriptor(RemoteNativeMpcDiscovery)); +mcpDiscoveryRegistry.register(new SyncDescriptor(ConfigMcpDiscovery)); +mcpDiscoveryRegistry.register(new SyncDescriptor(ExtensionMcpDiscovery)); + +registerWorkbenchContribution2('mcpDiscovery', McpDiscovery, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2('mcpContextKeys', McpContextKeysController, WorkbenchPhase.BlockRestore); + +registerAction2(ListMcpServerCommand); +registerAction2(McpServerOptionsCommand); +registerAction2(ResetMcpTrustCommand); +registerAction2(ResetMcpCachedTools); +registerAction2(AddConfigurationAction); + +registerWorkbenchContribution2('mcpActionRendering', MCPServerActionRendering, WorkbenchPhase.BlockRestore); + +const jsonRegistry = Registry.as(jsonContributionRegistry.Extensions.JSONContribution); +jsonRegistry.registerSchema(mcpSchemaId, mcpServerSchema); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts new file mode 100644 index 00000000000..fe14cebe379 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -0,0 +1,534 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h } from '../../../../base/browser/dom.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { groupBy } from '../../../../base/common/collections.js'; +import { Event } from '../../../../base/common/event.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { autorun, derived } from '../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { ILocalizedString, localize, localize2 } from '../../../../nls.js'; +import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; +import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { Action2, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ConfigurationTarget, getConfigValueInTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IMcpConfiguration, IMcpConfigurationSSE } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; +import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js'; +import { IWorkspace, IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { ActiveEditorContext, ResourceContextKey } from '../../../common/contextkeys.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { ChatMode } from '../../chat/common/constants.js'; +import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js'; +import { IMcpConfigurationStdio } from '../common/mcpConfiguration.js'; +import { McpContextKeys } from '../common/mcpContextKeys.js'; +import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; +import { IMcpServer, IMcpService, LazyCollectionState, McpConnectionState, McpServerToolsState } from '../common/mcpTypes.js'; + +// acroynms do not get localized +const category: ILocalizedString = { + original: 'MCP', + value: 'MCP', +}; + +export class ListMcpServerCommand extends Action2 { + public static readonly id = 'workbench.mcp.listServer'; + constructor() { + super({ + id: ListMcpServerCommand.id, + title: localize2('mcp.list', 'List Servers'), + icon: Codicon.server, + category, + f1: true, + menu: { + when: ContextKeyExpr.and( + ContextKeyExpr.or(McpContextKeys.hasUnknownTools, McpContextKeys.hasServersWithErrors), + ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent) + ), + id: MenuId.ChatInputAttachmentToolbar, + group: 'navigation', + order: 0 + }, + }); + } + + override async run(accessor: ServicesAccessor) { + const mcpService = accessor.get(IMcpService); + const commandService = accessor.get(ICommandService); + const quickInput = accessor.get(IQuickInputService); + + type ItemType = { id: string } & IQuickPickItem; + + const store = new DisposableStore(); + const pick = quickInput.createQuickPick({ useSeparators: true }); + pick.title = localize('mcp.selectServer', 'Select an MCP Server'); + + store.add(pick); + store.add(autorun(reader => { + const servers = groupBy(mcpService.servers.read(reader).slice().sort((a, b) => (a.collection.presentation?.order || 0) - (b.collection.presentation?.order || 0)), s => s.collection.id); + pick.items = Object.values(servers).flatMap(servers => { + return [ + { type: 'separator', label: servers[0].collection.label, id: servers[0].collection.id }, + ...servers.map(server => ({ + id: server.definition.id, + label: server.definition.label, + description: McpConnectionState.toString(server.connectionState.read(reader)), + })), + ]; + }); + })); + + + const picked = await new Promise(resolve => { + store.add(pick.onDidAccept(() => { + resolve(pick.activeItems[0]); + })); + store.add(pick.onDidHide(() => { + resolve(undefined); + })); + pick.show(); + }); + + store.dispose(); + + if (picked) { + commandService.executeCommand(McpServerOptionsCommand.id, picked.id); + } + } +} + + +export class McpServerOptionsCommand extends Action2 { + + static readonly id = 'workbench.mcp.serverOptions'; + + constructor() { + super({ + id: McpServerOptionsCommand.id, + title: localize2('mcp.options', 'Server Options'), + category, + f1: false, + }); + } + + override async run(accessor: ServicesAccessor, id: string): Promise { + const mcpService = accessor.get(IMcpService); + const quickInputService = accessor.get(IQuickInputService); + const server = mcpService.servers.get().find(s => s.definition.id === id); + if (!server) { + return; + } + + interface ActionItem extends IQuickPickItem { + action: 'start' | 'stop' | 'restart' | 'showOutput'; + } + + const items: ActionItem[] = []; + const serverState = server.connectionState.get(); + + // Only show start when server is stopped or in error state + if (McpConnectionState.canBeStarted(serverState.state)) { + items.push({ + label: localize2('mcp.start', 'Start Server').value, + action: 'start' + }); + } else { + items.push({ + label: localize2('mcp.stop', 'Stop Server').value, + action: 'stop' + }); + items.push({ + label: localize2('mcp.restart', 'Restart Server').value, + action: 'restart' + }); + } + + items.push({ + label: localize2('mcp.showOutput', 'Show Output').value, + action: 'showOutput' + }); + + const pick = await quickInputService.pick(items, { + title: server.definition.label, + placeHolder: localize('mcp.selectAction', 'Select Server Action') + }); + + if (!pick) { + return; + } + + switch (pick.action) { + case 'start': + await server.start(true); + server.showOutput(); + break; + case 'stop': + await server.stop(); + break; + case 'restart': + await server.stop(); + await server.start(true); + break; + case 'showOutput': + server.showOutput(); + break; + } + } +} + + +export class MCPServerActionRendering extends Disposable implements IWorkbenchContribution { + public static readonly ID = 'workbench.contrib.mcp.discovery'; + + constructor( + @IActionViewItemService actionViewItemService: IActionViewItemService, + @IMcpService mcpService: IMcpService, + @IInstantiationService instaService: IInstantiationService, + @ICommandService commandService: ICommandService, + ) { + super(); + + const enum DisplayedState { + None, + NewTools, + Error, + Refreshing, + } + + + + const displayedState = derived((reader) => { + const servers = mcpService.servers.read(reader); + const serversPerState: IMcpServer[][] = []; + for (const server of servers) { + let thisState = DisplayedState.None; + switch (server.toolsState.read(reader)) { + case McpServerToolsState.Unknown: + if (server.trusted.read(reader) === false) { + thisState = DisplayedState.None; + } else { + thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.NewTools; + } + break; + case McpServerToolsState.RefreshingFromUnknown: + thisState = DisplayedState.Refreshing; + break; + default: + thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.None; + break; + } + + serversPerState[thisState] ??= []; + serversPerState[thisState].push(server); + } + + const unknownServerStates = mcpService.lazyCollectionState.read(reader); + if (unknownServerStates === LazyCollectionState.LoadingUnknown) { + serversPerState[DisplayedState.Refreshing] ??= []; + } else if (unknownServerStates === LazyCollectionState.HasUnknown) { + serversPerState[DisplayedState.NewTools] ??= []; + } + + const maxState = (serversPerState.length - 1) as DisplayedState; + return { state: maxState, servers: serversPerState[maxState] || [] }; + }); + + this._store.add(actionViewItemService.register(MenuId.ChatInputAttachmentToolbar, ListMcpServerCommand.id, (action, options) => { + if (!(action instanceof MenuItemAction)) { + return undefined; + } + + return instaService.createInstance(class extends MenuEntryActionViewItem { + + override render(container: HTMLElement): void { + + super.render(container); + container.classList.add('chat-mcp'); + + const action = h('button.chat-mcp-action', [h('span@icon')]); + + this._register(autorun(r => { + const { state } = displayedState.read(r); + const { root, icon } = action; + this.updateTooltip(); + container.classList.toggle('chat-mcp-has-action', state !== DisplayedState.None); + + if (!root.parentElement) { + container.appendChild(root); + } + + root.ariaLabel = this.getLabelForState(displayedState.read(r)); + root.className = 'chat-mcp-action'; + icon.className = ''; + if (state === DisplayedState.NewTools) { + root.classList.add('chat-mcp-action-new'); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.refresh)); + } else if (state === DisplayedState.Error) { + root.classList.add('chat-mcp-action-error'); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.warning)); + } else if (state === DisplayedState.Refreshing) { + root.classList.add('chat-mcp-action-refreshing'); + icon.classList.add(...ThemeIcon.asClassNameArray(spinningLoading)); + } else { + root.remove(); + } + })); + } + + override async onClick(e: MouseEvent): Promise { + e.preventDefault(); + e.stopPropagation(); + + const { state, servers } = displayedState.get(); + if (state === DisplayedState.NewTools) { + servers.forEach(server => server.start()); + mcpService.activateCollections(); + } else if (state === DisplayedState.Refreshing) { + servers.at(-1)?.showOutput(); + } else if (state === DisplayedState.Error) { + const server = servers.at(-1); + if (server) { + commandService.executeCommand(McpServerOptionsCommand.id, server.definition.id); + } + } else { + commandService.executeCommand(ListMcpServerCommand.id); + } + } + + protected override getTooltip(): string { + return this.getLabelForState() || super.getTooltip(); + } + + private getLabelForState({ state, servers } = displayedState.get()) { + if (state === DisplayedState.NewTools) { + return localize('mcp.newTools', "New tools available ({0})", servers.length || 1); + } else if (state === DisplayedState.Error) { + return localize('mcp.toolError', "Error loading {0} tool(s)", servers.length || 1); + } else if (state === DisplayedState.Refreshing) { + return localize('mcp.toolRefresh', "Discovering tools..."); + } else { + return null; + } + } + + + }, action, { ...options, keybindingNotRenderedWithLabel: true }); + + }, Event.fromObservable(displayedState))); + } +} + +export class ResetMcpTrustCommand extends Action2 { + static readonly ID = 'workbench.mcp.resetTrust'; + + constructor() { + super({ + id: ResetMcpTrustCommand.ID, + title: localize2('mcp.resetTrust', "Reset Trust"), + category, + f1: true, + precondition: McpContextKeys.toolsCount.greater(0), + }); + } + + run(accessor: ServicesAccessor): void { + const mcpService = accessor.get(IMcpRegistry); + mcpService.resetTrust(); + } +} + + +export class ResetMcpCachedTools extends Action2 { + static readonly ID = 'workbench.mcp.resetCachedTools'; + + constructor() { + super({ + id: ResetMcpCachedTools.ID, + title: localize2('mcp.resetCachedTools', "Reset Cached Tools"), + category, + f1: true, + precondition: McpContextKeys.toolsCount.greater(0), + }); + } + + run(accessor: ServicesAccessor): void { + const mcpService = accessor.get(IMcpService); + mcpService.resetCaches(); + } +} + +export class AddConfigurationAction extends Action2 { + static readonly ID = 'workbench.mcp.addConfiguration'; + + constructor() { + super({ + id: AddConfigurationAction.ID, + title: localize2('mcp.addConfiguration', "Add Server..."), + metadata: { + description: localize2('mcp.addConfiguration.description', "Installs a new Model Context protocol to the mcp.json settings"), + }, + category, + f1: true, + menu: { + id: MenuId.EditorContent, + when: ContextKeyExpr.and( + ContextKeyExpr.regex(ResourceContextKey.Path.key, /\.vscode[/\\]mcp\.json$/), + ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID) + ) + } + }); + } + + private async getServerType(quickInputService: IQuickInputService): Promise<{ id: 'stdio' | 'sse' } | undefined> { + return quickInputService.pick([ + { id: 'stdio', label: localize('mcp.serverType.command', "Command (stdio)"), description: localize('mcp.serverType.command.description', "Run a local command that implements the MCP protocol") }, + { id: 'sse', label: localize('mcp.serverType.http', "HTTP (server-sent events)"), description: localize('mcp.serverType.http.description', "Connect to a remote HTTP server that implements the MCP protocol") } + ], { + title: localize('mcp.serverType.title', "Select Server Type"), + placeHolder: localize('mcp.serverType.placeholder', "Choose the type of MCP server to add") + }) as Promise<{ id: 'stdio' | 'sse' } | undefined>; + } + + private async getStdioConfig(quickInputService: IQuickInputService): Promise { + const command = await quickInputService.input({ + title: localize('mcp.command.title', "Enter Command"), + placeHolder: localize('mcp.command.placeholder', "Command to run (with optional arguments)"), + ignoreFocusLost: true, + }); + + if (!command) { + return undefined; + } + + // Split command into command and args, handling quotes + const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g)!; + return { + type: 'stdio', + command: parts[0].replace(/"/g, ''), + args: parts.slice(1).map(arg => arg.replace(/"/g, '')) + }; + } + + private async getSSEConfig(quickInputService: IQuickInputService): Promise { + const url = await quickInputService.input({ + title: localize('mcp.url.title', "Enter Server URL"), + placeHolder: localize('mcp.url.placeholder', "URL of the MCP server (e.g., http://localhost:3000)"), + ignoreFocusLost: true, + }); + + if (!url) { + return undefined; + } + + return { + type: 'sse', + url + }; + } + + private async getServerId(quickInputService: IQuickInputService): Promise { + const suggestedId = `my-mcp-server-${generateUuid().split('-')[0]}`; + const id = await quickInputService.input({ + title: localize('mcp.serverId.title', "Enter Server ID"), + placeHolder: localize('mcp.serverId.placeholder', "Unique identifier for this server"), + value: suggestedId, + ignoreFocusLost: true, + }); + + return id; + } + + private async getConfigurationTarget(quickInputService: IQuickInputService, workspace: IWorkspace, isInRemote: boolean): Promise { + const options: (IQuickPickItem & { target: ConfigurationTarget })[] = [ + { target: ConfigurationTarget.USER, label: localize('mcp.target.user', "User Settings"), description: localize('mcp.target.user.description', "Available in all workspaces") } + ]; + + if (isInRemote) { + options.push({ target: ConfigurationTarget.USER_REMOTE, label: localize('mcp.target.remote', "Remote Settings"), description: localize('mcp.target..remote.description', "Available on this remote machine") }); + } + + if (workspace.folders.length > 0) { + options.push({ target: ConfigurationTarget.WORKSPACE, label: localize('mcp.target.workspace', "Workspace Settings"), description: localize('mcp.target.workspace.description', "Available in this workspace") }); + } + + if (options.length === 1) { + return options[0].target; + } + + + const targetPick = await quickInputService.pick(options, { + title: localize('mcp.target.title', "Choose where to save the configuration"), + }); + + return targetPick?.target; + } + + async run(accessor: ServicesAccessor, configUri?: string): Promise { + const quickInputService = accessor.get(IQuickInputService); + const configurationService = accessor.get(IConfigurationService); + const jsonEditingService = accessor.get(IJSONEditingService); + const workspaceService = accessor.get(IWorkspaceContextService); + const environmentService = accessor.get(IWorkbenchEnvironmentService); + + // Step 1: Choose server type + const serverType = await this.getServerType(quickInputService); + if (!serverType) { + return; + } + + // Step 2: Get server details based on type + const serverConfig = await (serverType.id === 'stdio' + ? this.getStdioConfig(quickInputService) + : this.getSSEConfig(quickInputService)); + + if (!serverConfig) { + return; + } + + // Step 3: Get server ID + const serverId = await this.getServerId(quickInputService); + if (!serverId) { + return; + } + + // Step 4: Choose configuration target if no configUri provided + let target: ConfigurationTarget | undefined; + const workspace = workspaceService.getWorkspace(); + if (!configUri) { + target = await this.getConfigurationTarget(quickInputService, workspace, !!environmentService.remoteAuthority); + if (!target) { + return; + } + } + + // Step 5: Update configuration + const writeToUriDirect = configUri + ? URI.parse(configUri) + : target === ConfigurationTarget.WORKSPACE && workspace.folders.length === 1 + ? URI.joinPath(workspace.folders[0].uri, '.vscode', 'mcp.json') + : undefined; + + if (writeToUriDirect) { + await jsonEditingService.write(writeToUriDirect, [{ + path: ['servers', serverId], + value: serverConfig + }], true); + } else { + const settings: IMcpConfiguration = { ...getConfigValueInTarget(configurationService.inspect('mcp'), target!) }; + settings.servers ??= {}; + settings.servers[serverId] = serverConfig; + await configurationService.updateValue('mcp', settings, target!); + } + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpDiscovery.ts b/src/vs/workbench/contrib/mcp/browser/mcpDiscovery.ts new file mode 100644 index 00000000000..f9d339b7962 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpDiscovery.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js'; + +export class McpDiscovery extends Disposable implements IWorkbenchContribution { + public static readonly ID = 'workbench.contrib.mcp.discovery'; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + for (const discovery of mcpDiscoveryRegistry.getAll()) { + const inst = this._register(instantiationService.createInstance(discovery)); + inst.start(); + } + } +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts new file mode 100644 index 00000000000..04931acfe3d --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equals as arrayEquals } from '../../../../../base/common/arrays.js'; +import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; +import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { StorageScope } from '../../../../../platform/storage/common/storage.js'; +import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js'; +import { IMcpConfiguration, mcpConfigurationSection } from '../mcpConfiguration.js'; +import { IMcpRegistry } from '../mcpRegistryTypes.js'; +import { McpCollectionSortOrder, McpServerDefinition, McpServerTransportType } from '../mcpTypes.js'; +import { IMcpDiscovery } from './mcpDiscovery.js'; + + +/** + * Discovers MCP servers based on various config sources. + */ +export class ConfigMcpDiscovery extends Disposable implements IMcpDiscovery { + private readonly configSources: { + key: 'userLocalValue' | 'userRemoteValue' | 'workspaceValue'; + label: string; + serverDefinitions: ISettableObservable; + scope: StorageScope; + target: ConfigurationTarget; + disposable: MutableDisposable; + order: number; + remoteAuthority?: string; + }[]; + + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + @IProductService productService: IProductService, + @ILabelService labelService: ILabelService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + ) { + super(); + const remoteLabel = environmentService.remoteAuthority ? labelService.getHostLabel(Schemas.vscodeRemote, environmentService.remoteAuthority) : 'Remote'; + this.configSources = [ + { + key: 'userLocalValue', + target: ConfigurationTarget.USER_LOCAL, + label: localize('mcp.configuration.userLocalValue', 'Global in {0}', productService.nameShort), + serverDefinitions: observableValue(this, []), + scope: StorageScope.PROFILE, + disposable: this._register(new MutableDisposable()), + order: McpCollectionSortOrder.User, + }, + { + key: 'userRemoteValue', + target: ConfigurationTarget.USER_REMOTE, + label: localize('mcp.configuration.userRemoteValue', 'From {0}', remoteLabel), + serverDefinitions: observableValue(this, []), + scope: StorageScope.PROFILE, + disposable: this._register(new MutableDisposable()), + remoteAuthority: environmentService.remoteAuthority, + order: McpCollectionSortOrder.User + McpCollectionSortOrder.RemotePenalty, + }, + { + key: 'workspaceValue', + target: ConfigurationTarget.WORKSPACE, + label: localize('mcp.configuration.workspaceValue', 'From your workspace'), + serverDefinitions: observableValue(this, []), + scope: StorageScope.WORKSPACE, + disposable: this._register(new MutableDisposable()), + order: McpCollectionSortOrder.Workspace, + + }, + ]; + } + + public start() { + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(mcpConfigurationSection)) { + this.sync(); + } + })); + + this.sync(); + } + + private sync() { + const configurationKey = this._configurationService.inspect(mcpConfigurationSection); + + for (const src of this.configSources) { + const collectionId = `mcp.config.${src.key}`; + let value = configurationKey[src.key]; + + // If we see there are MCP servers, migrate them automatically + if (value?.mcpServers) { + value = { ...value, servers: { ...value.servers, ...value.mcpServers }, mcpServers: undefined }; + this._configurationService.updateValue(mcpConfigurationSection, value, {}, src.target, { donotNotifyError: true }); + } + + const nextDefinitions = Object.entries(value?.servers || {}).map(([name, value]): McpServerDefinition => ({ + id: `${collectionId}.${name}`, + label: name, + launch: 'type' in value && value.type === 'sse' ? { + type: McpServerTransportType.SSE, + uri: URI.parse(value.url), + headers: Object.entries(value.headers || {}), + } : { + type: McpServerTransportType.Stdio, + args: value.args || [], + command: value.command, + env: value.env || {}, + cwd: undefined, + }, + variableReplacement: { + section: mcpConfigurationSection, + target: src.target, + } + })); + + if (arrayEquals(nextDefinitions, src.serverDefinitions.get(), McpServerDefinition.equals)) { + continue; + } + + + if (!nextDefinitions.length) { + src.disposable.clear(); + src.serverDefinitions.set(nextDefinitions, undefined); + } else { + src.serverDefinitions.set(nextDefinitions, undefined); + src.disposable.value ??= this._mcpRegistry.registerCollection({ + id: collectionId, + label: src.label, + presentation: { order: src.order }, + remoteAuthority: src.remoteAuthority || null, + serverDefinitions: src.serverDefinitions, + isTrustedByDefault: true, + scope: src.scope, + }); + } + } + } +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.ts new file mode 100644 index 00000000000..75cbd2081f6 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../base/common/observable.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; +import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; +import { mcpActivationEvent, mcpContributionPoint } from '../mcpConfiguration.js'; +import { IMcpRegistry } from '../mcpRegistryTypes.js'; +import { extensionPrefixedIdentifier, McpServerDefinition } from '../mcpTypes.js'; +import { IMcpDiscovery } from './mcpDiscovery.js'; + +const cacheKey = 'mcp.extCachedServers'; + +interface IServerCacheEntry { + readonly servers: readonly McpServerDefinition.Serialized[]; +} + +const _mcpExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint(mcpContributionPoint); + +export class ExtensionMcpDiscovery extends Disposable implements IMcpDiscovery { + private readonly _extensionCollectionIdsToPersist = new Set(); + private readonly cachedServers: { [collcetionId: string]: IServerCacheEntry }; + + constructor( + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + @IStorageService storageService: IStorageService, + @IExtensionService private readonly _extensionService: IExtensionService, + ) { + super(); + this.cachedServers = storageService.getObject(cacheKey, StorageScope.WORKSPACE, {}); + + this._register(storageService.onWillSaveState(() => { + let updated = false; + for (const collectionId of this._extensionCollectionIdsToPersist) { + const collection = this._mcpRegistry.collections.get().find(c => c.id === collectionId); + if (!collection || collection.lazy) { + continue; + } + + const defs = collection.serverDefinitions.get(); + if (defs) { + updated = true; + this.cachedServers[collectionId] = { servers: defs.map(McpServerDefinition.toSerialized) }; + } + } + + if (updated) { + storageService.store(cacheKey, this.cachedServers, StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + })); + } + + public start(): void { + const extensionCollections = this._register(new DisposableMap()); + this._register(_mcpExtensionPoint.setHandler((_extensions, delta) => { + const { added, removed } = delta; + + for (const collections of removed) { + for (const coll of collections.value) { + extensionCollections.deleteAndDispose(extensionPrefixedIdentifier(collections.description.identifier, coll.id)); + } + } + + for (const collections of added) { + for (const coll of collections.value) { + const id = extensionPrefixedIdentifier(collections.description.identifier, coll.id); + this._extensionCollectionIdsToPersist.add(id); + + const serverDefs = this.cachedServers.hasOwnProperty(id) ? this.cachedServers[id].servers : undefined; + const dispo = this._mcpRegistry.registerCollection({ + id, + label: coll.label, + remoteAuthority: null, + isTrustedByDefault: true, + scope: StorageScope.WORKSPACE, + serverDefinitions: observableValue(this, serverDefs?.map(McpServerDefinition.fromSerialized) || []), + lazy: { + isCached: !!serverDefs, + load: () => this._activateExtensionServers(coll.id), + removed: () => extensionCollections.deleteAndDispose(id), + } + }); + + extensionCollections.set(id, dispo); + } + } + })); + } + + private async _activateExtensionServers(collectionId: string): Promise { + await this._extensionService.activateByEvent(mcpActivationEvent(collectionId)); + await Promise.all(this._mcpRegistry.delegates + .map(r => r.waitForInitialProviderPromises())); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/mcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/mcpDiscovery.ts new file mode 100644 index 00000000000..a6a091cfaa7 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/mcpDiscovery.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { SyncDescriptor0 } from '../../../../../platform/instantiation/common/descriptors.js'; + + +export interface IMcpDiscovery extends IDisposable { + start(): void; +} + +class McpDiscoveryRegistry { + private readonly _discovery: SyncDescriptor0[] = []; + + register(discovery: SyncDescriptor0): void { + this._discovery.push(discovery); + } + + getAll(): readonly SyncDescriptor0[] { + return this._discovery; + } +} + +export const mcpDiscoveryRegistry = new McpDiscoveryRegistry(); + + + diff --git a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAbstract.ts b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAbstract.ts new file mode 100644 index 00000000000..803439a8b83 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAbstract.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { autorunWithStore, IObservable, observableValue } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { INativeMcpDiscoveryData } from '../../../../../platform/mcp/common/nativeMcpDiscoveryHelper.js'; +import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { StorageScope } from '../../../../../platform/storage/common/storage.js'; +import { Dto } from '../../../../services/extensions/common/proxyIdentifier.js'; +import { mcpDiscoverySection } from '../mcpConfiguration.js'; +import { IMcpRegistry } from '../mcpRegistryTypes.js'; +import { McpCollectionDefinition, McpCollectionSortOrder, McpServerDefinition } from '../mcpTypes.js'; +import { IMcpDiscovery } from './mcpDiscovery.js'; +import { ClaudeDesktopMpcDiscoveryAdapter, NativeMpcDiscoveryAdapter } from './nativeMcpDiscoveryAdapters.js'; + +/** + * Base class that discovers MCP servers on a filesystem, outside of the ones + * defined in VS Code settings. + */ +export abstract class FilesystemMpcDiscovery extends Disposable implements IMcpDiscovery { + private readonly adapters: readonly NativeMpcDiscoveryAdapter[]; + private _fsDiscoveryEnabled: IObservable; + private suffix = ''; + + constructor( + remoteAuthority: string | null, + @ILabelService labelService: ILabelService, + @IFileService private readonly fileService: IFileService, + @IInstantiationService instantiationService: IInstantiationService, + @IMcpRegistry private readonly mcpRegistry: IMcpRegistry, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(); + if (remoteAuthority) { + this.suffix = ' ' + localize('onRemoteLabel', ' on {0}', labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority)); + } + + this._fsDiscoveryEnabled = observableConfigValue(mcpDiscoverySection, false, configurationService); + + this.adapters = [ + instantiationService.createInstance(ClaudeDesktopMpcDiscoveryAdapter, remoteAuthority) + ]; + } + + public abstract start(): void; + + protected setDetails(detailsDto: Dto | undefined) { + if (!detailsDto) { + return; + } + + const details: INativeMcpDiscoveryData = { + ...detailsDto, + homedir: URI.revive(detailsDto.homedir), + xdgHome: detailsDto.xdgHome ? URI.revive(detailsDto.xdgHome) : undefined, + winAppData: detailsDto.winAppData ? URI.revive(detailsDto.winAppData) : undefined, + }; + + for (const adapter of this.adapters) { + const file = adapter.getFilePath(details); + if (!file) { + continue; + } + + const collection = { + id: adapter.id, + label: adapter.label + this.suffix, + remoteAuthority: adapter.remoteAuthority, + scope: StorageScope.PROFILE, + isTrustedByDefault: false, + serverDefinitions: observableValue(this, []), + presentation: { + origin: file, + order: adapter.order + (adapter.remoteAuthority ? McpCollectionSortOrder.RemotePenalty : 0), + }, + } satisfies McpCollectionDefinition; + + const collectionRegistration = this._register(new MutableDisposable()); + const updateFile = async () => { + let definitions: McpServerDefinition[] = []; + try { + const contents = await this.fileService.readFile(file); + definitions = adapter.adaptFile(contents.value, details) || []; + } catch { + // ignored + } + if (!definitions.length) { + collectionRegistration.clear(); + } else { + collection.serverDefinitions.set(definitions, undefined); + if (!collectionRegistration.value) { + collectionRegistration.value = this.mcpRegistry.registerCollection(collection); + } + } + }; + + this._register(autorunWithStore((reader, store) => { + if (!this._fsDiscoveryEnabled.read(reader)) { + collectionRegistration.clear(); + return; + } + + const throttler = store.add(new RunOnceScheduler(updateFile, 500)); + const watcher = store.add(this.fileService.createWatcher(file, { recursive: false, excludes: [] })); + store.add(watcher.onDidChange(() => throttler.schedule())); + updateFile(); + })); + } + } +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts new file mode 100644 index 00000000000..8eb01b9c722 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { Platform } from '../../../../../base/common/platform.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { INativeMcpDiscoveryData } from '../../../../../platform/mcp/common/nativeMcpDiscoveryHelper.js'; +import { McpCollectionSortOrder, McpServerDefinition, McpServerTransportType } from '../mcpTypes.js'; + +export interface NativeMpcDiscoveryAdapter { + readonly remoteAuthority: string | null; + readonly id: string; + readonly label: string; + readonly order: number; + + getFilePath(details: INativeMcpDiscoveryData): URI | undefined; + adaptFile(contents: VSBuffer, details: INativeMcpDiscoveryData): McpServerDefinition[] | undefined; +} + +export class ClaudeDesktopMpcDiscoveryAdapter implements NativeMpcDiscoveryAdapter { + public readonly id: string; + public readonly label: string = 'Claude Desktop'; + public readonly order = McpCollectionSortOrder.Filesystem; + + constructor(public readonly remoteAuthority: string | null) { + this.id = `claude-desktop.${this.remoteAuthority}`; + } + + getFilePath({ platform, winAppData, xdgHome, homedir }: INativeMcpDiscoveryData): URI | undefined { + if (platform === Platform.Windows) { + const appData = winAppData || URI.joinPath(homedir, 'AppData', 'Roaming'); + return URI.joinPath(appData, 'Claude', 'claude_desktop_config.json'); + } else if (platform === Platform.Mac) { + return URI.joinPath(homedir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'); + } else { + const configDir = xdgHome || URI.joinPath(homedir, '.config'); + return URI.joinPath(configDir, 'Claude', 'claude_desktop_config.json'); + } + } + + adaptFile(contents: VSBuffer, { homedir }: INativeMcpDiscoveryData): McpServerDefinition[] | undefined { + let parsed: { + mcpServers: Record; + }>; + }; + + try { + parsed = JSON.parse(contents.toString()); + } catch { + return; + } + return Object.entries(parsed.mcpServers).map(([name, server]): McpServerDefinition => { + return { + id: `claude_desktop_config.${name}`, + label: name, + launch: { + type: McpServerTransportType.Stdio, + args: server.args || [], + command: server.command, + env: server.env || {}, + cwd: homedir, + } + }; + }); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpRemoteDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpRemoteDiscovery.ts new file mode 100644 index 00000000000..4f525561f6f --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpRemoteDiscovery.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProxyChannel } from '../../../../../base/parts/ipc/common/ipc.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { INativeMcpDiscoveryHelperService, NativeMcpDiscoveryHelperChannelName } from '../../../../../platform/mcp/common/nativeMcpDiscoveryHelper.js'; +import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js'; +import { IMcpRegistry } from '../mcpRegistryTypes.js'; +import { FilesystemMpcDiscovery } from './nativeMcpDiscoveryAbstract.js'; + +/** + * Discovers MCP servers on the remote filesystem, if any. + */ +export class RemoteNativeMpcDiscovery extends FilesystemMpcDiscovery { + constructor( + @IRemoteAgentService private readonly remoteAgent: IRemoteAgentService, + @ILogService private readonly logService: ILogService, + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService, + @IInstantiationService instantiationService: IInstantiationService, + @IMcpRegistry mcpRegistry: IMcpRegistry, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(remoteAgent.getConnection()?.remoteAuthority || null, labelService, fileService, instantiationService, mcpRegistry, configurationService); + } + + public override async start() { + const connection = this.remoteAgent.getConnection(); + if (!connection) { + return this.setDetails(undefined); + } + + await connection.withChannel(NativeMcpDiscoveryHelperChannelName, async channel => { + const service = ProxyChannel.toService(channel); + + service.load().then( + data => this.setDetails(data), + err => { + this.logService.warn('Error getting remote process MCP environment', err); + this.setDetails(undefined); + } + ); + }); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts new file mode 100644 index 00000000000..fcef4dc9d48 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; +import { localize } from '../../../../nls.js'; +import { IMcpCollectionContribution } from '../../../../platform/extensions/common/extensions.js'; +import { mcpSchemaId } from '../../../services/configuration/common/configuration.js'; +import { inputsSchema } from '../../../services/configurationResolver/common/configurationResolverSchema.js'; +import { IExtensionPointDescriptor } from '../../../services/extensions/common/extensionsRegistry.js'; + +export type { McpConfigurationServer, IMcpConfigurationStdio, IMcpConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; + +const mcpActivationEventPrefix = 'onMcpCollection:'; + +export const mcpActivationEvent = (collectionId: string) => mcpActivationEventPrefix + collectionId; + +const mcpSchemaExampleServer = { + command: 'node', + args: ['my-mcp-server.js'], + env: {}, +}; + +export const mcpConfigurationSection = 'mcp'; +export const mcpDiscoverySection = 'chat.mcp.discovery.enabled'; + +export const mcpSchemaExampleServers = { + 'mcp-server-time': { + command: 'python', + args: ['-m', 'mcp_server_time', '--local-timezone=America/Los_Angeles'], + env: {}, + } +}; + +export const mcpServerSchema: IJSONSchema = { + id: mcpSchemaId, + type: 'object', + title: localize('app.mcp.json.title', "Model Context Protocol Servers"), + allowTrailingCommas: true, + allowComments: true, + additionalProperties: false, + properties: { + servers: { + examples: [mcpSchemaExampleServers], + additionalProperties: { + oneOf: [{ + type: 'object', + additionalProperties: false, + examples: [mcpSchemaExampleServer], + properties: { + type: { + type: 'string', + enum: ['stdio'], + description: localize('app.mcp.json.type', "The type of the server.") + }, + command: { + type: 'string', + description: localize('app.mcp.json.command', "The command to run the server.") + }, + args: { + type: 'array', + description: localize('app.mcp.args.command', "Arguments passed to the server."), + items: { + type: 'string' + }, + }, + env: { + description: localize('app.mcp.env.command', "Environment variables passed to the server."), + additionalProperties: { + anyOf: [ + { type: 'null' }, + { type: 'string' }, + { type: 'number' }, + ] + } + }, + } + }, { + type: 'object', + additionalProperties: false, + required: ['url', 'type'], + examples: [{ + type: 'sse', + url: 'http://localhost:3001', + headers: {}, + }], + properties: { + type: { + type: 'string', + enum: ['sse'], + description: localize('app.mcp.json.type', "The type of the server.") + }, + url: { + type: 'string', + format: 'uri', + description: localize('app.mcp.json.url', "The URL of the server-sent-event (SSE) server.") + }, + env: { + description: localize('app.mcp.json.headers', "Additional headers sent to the server."), + additionalProperties: { type: 'string' }, + }, + } + }] + } + }, + inputs: inputsSchema.definitions!.inputs + } +}; + +export const mcpContributionPoint: IExtensionPointDescriptor = { + extensionPoint: 'modelContextServerCollections', + activationEventsGenerator(contribs, result) { + for (const contrib of contribs) { + if (contrib.id) { + result.push(mcpActivationEvent(contrib.id)); + } + } + }, + jsonSchema: { + description: localize('vscode.extension.contributes.mcp', 'Contributes Model Context Protocol servers. Users of this should also use `vscode.lm.registerMcpConfigurationProvider`.'), + type: 'array', + defaultSnippets: [{ body: [{ id: '', label: '' }] }], + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { id: '', label: '' } }], + properties: { + id: { + description: localize('vscode.extension.contributes.mcp.id', "Unique ID for the collection."), + type: 'string' + }, + label: { + description: localize('vscode.extension.contributes.mcp.label', "Display name for the collection."), + type: 'string' + } + } + } + } +}; diff --git a/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts b/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts new file mode 100644 index 00000000000..0d0aff9aa6d --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { localize } from '../../../../nls.js'; +import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { LazyCollectionState, IMcpService, McpServerToolsState, McpConnectionState } from './mcpTypes.js'; + + +export namespace McpContextKeys { + + export const serverCount = new RawContextKey('mcp.serverCount', undefined, { type: 'number', description: localize('mcp.serverCount.description', "Context key that has the number of registered MCP servers") }); + export const hasUnknownTools = new RawContextKey('mcp.hasUnknownTools', undefined, { type: 'boolean', description: localize('mcp.hasUnknownTools.description', "Indicates whether there are MCP servers with unknown tools.") }); + /** + * A context key that indicates whether there are any servers with errors. + * + * @type {boolean} + * @default undefined + * @description This key is used to track the presence of servers with errors in the MCP context. + */ + export const hasServersWithErrors = new RawContextKey('mcp.hasServersWithErrors', undefined, { type: 'boolean', description: localize('mcp.hasServersWithErrors.description', "Indicates whether there are any MCP servers with errors.") }); + export const toolsCount = new RawContextKey('mcp.toolsCount', undefined, { type: 'number', description: localize('mcp.toolsCount.description', "Context key that has the number of registered MCP tools") }); +} + + +export class McpContextKeysController extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.mcp.contextKey'; + + constructor( + @IMcpService mcpService: IMcpService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(); + + const ctxServerCount = McpContextKeys.serverCount.bindTo(contextKeyService); + const ctxToolsCount = McpContextKeys.toolsCount.bindTo(contextKeyService); + const ctxHasUnknownTools = McpContextKeys.hasUnknownTools.bindTo(contextKeyService); + + this._store.add(bindContextKey(McpContextKeys.hasServersWithErrors, contextKeyService, r => mcpService.servers.read(r).some(c => c.connectionState.read(r).state === McpConnectionState.Kind.Error))); + + this._store.add(autorun(r => { + const servers = mcpService.servers.read(r); + const serverTools = servers.map(s => s.tools.read(r)); + ctxServerCount.set(servers.length); + ctxToolsCount.set(serverTools.reduce((count, tools) => count + tools.length, 0)); + ctxHasUnknownTools.set(mcpService.lazyCollectionState.read(r) !== LazyCollectionState.AllKnown || servers.some(s => { + if (s.trusted.read(r) === false) { + return false; + } + + const toolState = s.toolsState.read(r); + return toolState === McpServerToolsState.Unknown || toolState === McpServerToolsState.RefreshingFromUnknown; + })); + })); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts new file mode 100644 index 00000000000..6ae03b715b6 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts @@ -0,0 +1,278 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { StringSHA1 } from '../../../../base/common/hash.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Lazy } from '../../../../base/common/lazy.js'; +import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { derived, IObservable, observableValue } from '../../../../base/common/observable.js'; +import { basename } from '../../../../base/common/resources.js'; +import { localize } from '../../../../nls.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { observableMemento } from '../../../../platform/observable/common/observableMemento.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js'; +import { McpRegistryInputStorage } from './mcpRegistryInputStorage.js'; +import { IMcpHostDelegate, IMcpRegistry, IMcpResolveConnectionOptions } from './mcpRegistryTypes.js'; +import { McpServerConnection } from './mcpServerConnection.js'; +import { IMcpServerConnection, LazyCollectionState, McpCollectionDefinition, McpCollectionReference } from './mcpTypes.js'; + +const createTrustMemento = observableMemento>>({ + defaultValue: {}, + key: 'mcp.trustedCollections' +}); + +const collectionPrefixLen = 3; + +export class McpRegistry extends Disposable implements IMcpRegistry { + declare public readonly _serviceBrand: undefined; + + private readonly _trustPrompts = new Map>(); + + private readonly _collections = observableValue('collections', []); + private readonly _delegates: IMcpHostDelegate[] = []; + public readonly collections: IObservable = this._collections; + + private readonly _collectionToPrefixes = this._collections.map(c => { + // This creates tool prefixes based on a hash of the collection ID. This is + // a short prefix because tool names that are too long can cause errors (#243602). + // So we take a hash (in order for tools to be stable, because randomized + // names can cause hallicinations if present in history) and then adjust + // them if there are any collisions. + type CollectionHash = { view: number; hash: string; collection: McpCollectionDefinition }; + + const hashes = c.map((collection): CollectionHash => { + const sha = new StringSHA1(); + sha.update(collection.id); + return { view: 0, hash: sha.digest(), collection }; + }); + + const view = (h: CollectionHash) => h.hash.slice(h.view, h.view + collectionPrefixLen); + + let collided = false; + do { + hashes.sort((a, b) => view(a).localeCompare(view(b)) || a.collection.id.localeCompare(b.collection.id)); + collided = false; + for (let i = 1; i < hashes.length; i++) { + const prev = hashes[i - 1]; + const curr = hashes[i]; + if (view(prev) === view(curr) && curr.view + collectionPrefixLen < curr.hash.length) { + curr.view++; + collided = true; + } + } + } while (collided); + + return Object.fromEntries(hashes.map(h => [h.collection.id, view(h) + '.'])); + }); + + private readonly _workspaceStorage = new Lazy(() => this._register(this._instantiationService.createInstance(McpRegistryInputStorage, StorageScope.WORKSPACE, StorageTarget.USER))); + private readonly _profileStorage = new Lazy(() => this._register(this._instantiationService.createInstance(McpRegistryInputStorage, StorageScope.PROFILE, StorageTarget.USER))); + + private readonly _trustMemento = new Lazy(() => this._register(createTrustMemento(StorageScope.APPLICATION, StorageTarget.MACHINE, this._storageService))); + private readonly _lazyCollectionsToUpdate = new Set(); + private readonly _ongoingLazyActivations = observableValue(this, 0); + + public readonly lazyCollectionState = derived(reader => { + if (this._ongoingLazyActivations.read(reader) > 0) { + return LazyCollectionState.LoadingUnknown; + } + const collections = this._collections.read(reader); + return collections.some(c => c.lazy && c.lazy.isCached === false) ? LazyCollectionState.HasUnknown : LazyCollectionState.AllKnown; + }); + + public get delegates(): readonly IMcpHostDelegate[] { + return this._delegates; + } + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, + @IDialogService private readonly _dialogService: IDialogService, + @IStorageService private readonly _storageService: IStorageService, + @IProductService private readonly _productService: IProductService, + ) { + super(); + } + + public registerDelegate(delegate: IMcpHostDelegate): IDisposable { + this._delegates.push(delegate); + return { + dispose: () => { + const index = this._delegates.indexOf(delegate); + if (index !== -1) { + this._delegates.splice(index, 1); + } + } + }; + } + + public registerCollection(collection: McpCollectionDefinition): IDisposable { + const currentCollections = this._collections.get(); + const toReplace = currentCollections.find(c => c.lazy && c.id === collection.id); + + // Incoming collections replace the "lazy" versions. See `ExtensionMcpDiscovery` for an example. + if (toReplace) { + this._lazyCollectionsToUpdate.add(collection.id); + this._collections.set(currentCollections.map(c => c === toReplace ? collection : c), undefined); + } else { + this._collections.set([...currentCollections, collection], undefined); + } + + return { + dispose: () => { + const currentCollections = this._collections.get(); + this._collections.set(currentCollections.filter(c => c !== collection), undefined); + } + }; + } + + public collectionToolPrefix(collection: McpCollectionReference): IObservable { + return this._collectionToPrefixes.map(p => p[collection.id] ?? ''); + } + + public async discoverCollections(): Promise { + const toDiscover = this._collections.get().filter(c => c.lazy && !c.lazy.isCached); + + this._ongoingLazyActivations.set(this._ongoingLazyActivations.get() + 1, undefined); + await Promise.all(toDiscover.map(c => c.lazy?.load())).finally(() => { + this._ongoingLazyActivations.set(this._ongoingLazyActivations.get() - 1, undefined); + }); + + const found: McpCollectionDefinition[] = []; + const current = this._collections.get(); + for (const collection of toDiscover) { + const rec = current.find(c => c.id === collection.id); + if (!rec) { + // ignored + } else if (rec.lazy) { + rec.lazy.removed?.(); // did not get replaced by the non-lazy version + } else { + found.push(rec); + } + } + + + return found; + } + + public clearSavedInputs() { + this._profileStorage.value.clearAll(); + this._workspaceStorage.value.clearAll(); + } + + public resetTrust(): void { + this._trustMemento.value.set({}, undefined); + } + + public getTrust(collectionRef: McpCollectionReference): IObservable { + return derived(reader => { + const collection = this._collections.read(reader).find(c => c.id === collectionRef.id); + if (!collection || collection.isTrustedByDefault) { + return true; + } + + const memento = this._trustMemento.value.read(reader); + return memento.hasOwnProperty(collection.id) ? memento[collection.id] : undefined; + }); + } + + private _promptForTrust(collection: McpCollectionDefinition): Promise { + // Collect all trust prompts for a single config so that concurrently trying to start N + // servers in a config don't result in N different dialogs + let resultPromise = this._trustPrompts.get(collection.id); + resultPromise ??= this._promptForTrustOpenDialog(collection).finally(() => { + this._trustPrompts.delete(collection.id); + }); + this._trustPrompts.set(collection.id, resultPromise); + + return resultPromise; + } + + private async _promptForTrustOpenDialog(collection: McpCollectionDefinition): Promise { + const labelWithOrigin = collection.presentation?.origin + ? `[\`${basename(collection.presentation.origin)}\`](${collection.presentation.origin})` + : collection.label; + const result = await this._dialogService.prompt( + { + message: localize('trustTitleWithOrigin', 'Trust MCP servers from {0}?', collection.label), + custom: { + markdownDetails: [{ + markdown: new MarkdownString(localize('mcp.trust.details', '{0} discovered Model Context Protocol servers from {1} (`{2}`). {0} can use their capabilities in Chat.\n\nDo you want to allow running MCP servers from {3}?', this._productService.nameShort, collection.label, collection.serverDefinitions.get().map(s => s.label).join('`, `'), labelWithOrigin)), + dismissOnLinkClick: true, + }] + }, + buttons: [ + { label: localize('mcp.trust.yes', 'Trust'), run: () => true }, + { label: localize('mcp.trust.no', 'Do not trust'), run: () => false } + ], + }, + ); + + return result.result; + } + + public async resolveConnection({ collectionRef, definitionRef, forceTrust }: IMcpResolveConnectionOptions): Promise { + const collection = this._collections.get().find(c => c.id === collectionRef.id); + const definition = collection?.serverDefinitions.get().find(s => s.id === definitionRef.id); + if (!collection || !definition) { + throw new Error(`Collection or definition not found for ${collectionRef.id} and ${definitionRef.id}`); + } + + const delegate = this._delegates.find(d => d.canStart(collection, definition)); + if (!delegate) { + throw new Error('No delegate found that can handle the connection'); + } + + if (!collection.isTrustedByDefault) { + const memento = this._trustMemento.value.get(); + const trusted = memento.hasOwnProperty(collection.id) ? memento[collection.id] : undefined; + + if (trusted) { + // continue + } else if (trusted === undefined || forceTrust) { + const trustValue = await this._promptForTrust(collection); + if (trustValue !== undefined) { + this._trustMemento.value.set({ ...memento, [collection.id]: trustValue }, undefined); + } + if (!trustValue) { + return; + } + } else /** trusted === false && !forceTrust */ { + return undefined; + } + } + + let launch = definition.launch; + + if (definition.variableReplacement) { + const inputStorage = definition.variableReplacement.folder ? this._workspaceStorage.value : this._profileStorage.value; + const previouslyStored = await inputStorage.getMap(); + + const { folder, section, target } = definition.variableReplacement; + + // based on _configurationResolverService.resolveWithInteractionReplace + launch = await this._configurationResolverService.resolveAnyAsync(folder, launch); + + const newVariables = await this._configurationResolverService.resolveWithInteraction(folder, launch, section, previouslyStored, target); + + if (newVariables?.size) { + const completeVariables = { ...previouslyStored, ...Object.fromEntries(newVariables) }; + launch = await this._configurationResolverService.resolveAnyAsync(folder, launch, completeVariables); + await inputStorage.setSecrets(completeVariables); + } + } + + return this._instantiationService.createInstance( + McpServerConnection, + collection, + definition, + delegate, + launch, + ); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts new file mode 100644 index 00000000000..6e23a059998 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts @@ -0,0 +1,192 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Sequencer } from '../../../../base/common/async.js'; +import { decodeBase64, encodeBase64, VSBuffer } from '../../../../base/common/buffer.js'; +import { Lazy } from '../../../../base/common/lazy.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { isEmptyObject } from '../../../../base/common/types.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; + +const MCP_ENCRYPTION_KEY_NAME = 'mcpEncryptionKey'; +const MCP_ENCRYPTION_KEY_ALGORITHM = 'AES-GCM'; +const MCP_ENCRYPTION_KEY_LEN = 256; +const MCP_ENCRYPTION_IV_LENGTH = 12; // 96 bits +const MCP_DATA_STORED_VERSION = 1; +const MCP_DATA_STORED_KEY = 'mcpInputs'; + +interface IStoredData { + version: number; + values: Record; + secrets?: { value: string; iv: string }; // base64, encrypted +} + +interface IHydratedData extends IStoredData { + unsealedSecrets?: Record; +} + +export class McpRegistryInputStorage extends Disposable { + private static secretSequencer = new Sequencer(); + private readonly _secretsSealerSequencer = new Sequencer(); + + private readonly _getEncryptionKey = new Lazy(() => { + return McpRegistryInputStorage.secretSequencer.queue(async () => { + const existing = await this._secretStorageService.get(MCP_ENCRYPTION_KEY_NAME); + if (existing) { + try { + const parsed: JsonWebKey = JSON.parse(existing); + return await crypto.subtle.importKey('jwk', parsed, MCP_ENCRYPTION_KEY_ALGORITHM, false, ['encrypt', 'decrypt']); + } catch { + // fall through + } + } + + const key = await crypto.subtle.generateKey( + { name: MCP_ENCRYPTION_KEY_ALGORITHM, length: MCP_ENCRYPTION_KEY_LEN }, + true, + ['encrypt', 'decrypt'], + ); + + const exported = await crypto.subtle.exportKey('jwk', key); + await this._secretStorageService.set(MCP_ENCRYPTION_KEY_NAME, JSON.stringify(exported)); + return key; + }); + }); + + private _didChange = false; + + private _record = new Lazy(() => { + const stored = this._storageService.getObject(MCP_DATA_STORED_KEY, this._scope); + return stored?.version === MCP_DATA_STORED_VERSION ? { ...stored } : { version: MCP_DATA_STORED_VERSION, values: {} }; + }); + + + constructor( + private readonly _scope: StorageScope, + _target: StorageTarget, + @IStorageService private readonly _storageService: IStorageService, + @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + + this._register(_storageService.onWillSaveState(() => { + if (this._didChange) { + this._storageService.store(MCP_DATA_STORED_KEY, { + version: MCP_DATA_STORED_VERSION, + values: this._record.value.values, + secrets: this._record.value.secrets, + } satisfies IStoredData, this._scope, _target); + this._didChange = false; + } + })); + } + + /** Deletes all collection data from storage. */ + public clearAll() { + this._record.value.values = {}; + this._record.value.secrets = undefined; + this._record.value.unsealedSecrets = undefined; + this._didChange = true; + } + + /** Delete a single collection data from the storage. */ + public async clear(inputKey: string) { + const secrets = await this._unsealSecrets(); + delete this._record.value.values[inputKey]; + this._didChange = true; + + if (secrets.hasOwnProperty(inputKey)) { + delete secrets[inputKey]; + await this._sealSecrets(); + } + } + + /** Gets a mapping of saved input data. */ + public async getMap() { + const secrets = await this._unsealSecrets(); + return { ...this._record.value.values, ...secrets }; + } + + /** Updates the input data mapping. */ + public async setPlainText(values: Record) { + Object.assign(this._record.value.values, values); + this._didChange = true; + } + + /** Updates the input secrets mapping. */ + public async setSecrets(values: Record) { + const unsealed = await this._unsealSecrets(); + Object.assign(unsealed, values); + await this._sealSecrets(); + } + + private async _sealSecrets() { + return this._secretsSealerSequencer.queue(async () => { + if (!this._record.value.unsealedSecrets || isEmptyObject(this._record.value.unsealedSecrets)) { + this._record.value.secrets = undefined; + return; + } + + if (!this._record.value.secrets) { + const iv = crypto.getRandomValues(new Uint8Array(MCP_ENCRYPTION_IV_LENGTH)); + this._record.value.secrets = { + value: '', + iv: encodeBase64(VSBuffer.wrap(iv)), + }; + } + + const toSeal = JSON.stringify(this._record.value.unsealedSecrets); + const iv = decodeBase64(this._record.value.secrets.iv); + const key = await this._getEncryptionKey.value; + const encrypted = await crypto.subtle.encrypt( + { name: MCP_ENCRYPTION_KEY_ALGORITHM, iv: iv.buffer }, + key, + new TextEncoder().encode(toSeal).buffer, + ); + + const enc = encodeBase64(VSBuffer.wrap(new Uint8Array(encrypted))); + if (this._record.value.secrets.value === enc) { + return; + } + + this._record.value.secrets.value = enc; + this._didChange = true; + }); + } + + private async _unsealSecrets(): Promise> { + if (!this._record.value.secrets) { + return this._record.value.unsealedSecrets ??= {}; + } + + if (this._record.value.unsealedSecrets) { + return this._record.value.unsealedSecrets; + } + + try { + const key = await this._getEncryptionKey.value; + const iv = decodeBase64(this._record.value.secrets.iv); + const encrypted = decodeBase64(this._record.value.secrets.value); + + const decrypted = await crypto.subtle.decrypt( + { name: MCP_ENCRYPTION_KEY_ALGORITHM, iv: iv.buffer }, + key, + encrypted.buffer, + ); + + const unsealedSecrets = JSON.parse(new TextDecoder().decode(decrypted)); + this._record.value.unsealedSecrets = unsealedSecrets; + return unsealedSecrets; + } catch (e) { + this._logService.warn('Error unsealing MCP secrets', e); + this._record.value.secrets = undefined; + } + + return {}; + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts new file mode 100644 index 00000000000..e835be770b8 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../base/common/observable.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { McpCollectionDefinition, McpServerDefinition, McpServerLaunch, McpConnectionState, IMcpServerConnection, McpCollectionReference, McpDefinitionReference, LazyCollectionState } from './mcpTypes.js'; +import { MCP } from './modelContextProtocol.js'; + +export const IMcpRegistry = createDecorator('mcpRegistry'); + +/** Message transport to a single MCP server. */ +export interface IMcpMessageTransport extends IDisposable { + readonly state: IObservable; + readonly onDidLog: Event; + readonly onDidReceiveMessage: Event; + send(message: MCP.JSONRPCMessage): void; + stop(): void; +} + +export interface IMcpHostDelegate { + waitForInitialProviderPromises(): Promise; + canStart(collectionDefinition: McpCollectionDefinition, serverDefinition: McpServerDefinition): boolean; + start(collectionDefinition: McpCollectionDefinition, serverDefinition: McpServerDefinition, resolvedLaunch: McpServerLaunch): IMcpMessageTransport; +} + +export interface IMcpResolveConnectionOptions { + collectionRef: McpCollectionReference; + definitionRef: McpDefinitionReference; + /** If set, the user will be asked to trust the collection even if they untrusted it previously */ + forceTrust?: boolean; +} + +export interface IMcpRegistry { + readonly _serviceBrand: undefined; + + readonly collections: IObservable; + readonly delegates: readonly IMcpHostDelegate[]; + + /** Gets the prefix that should be applied to a collection's tools in order to avoid ID conflicts */ + collectionToolPrefix(collection: McpCollectionReference): IObservable; + + /** Whether there are new collections that can be resolved with a discover() call */ + readonly lazyCollectionState: IObservable; + /** Discover new collections, returning newly-discovered ones. */ + discoverCollections(): Promise; + + registerDelegate(delegate: IMcpHostDelegate): IDisposable; + registerCollection(collection: McpCollectionDefinition): IDisposable; + + /** Resets the trust state of all collections. */ + resetTrust(): void; + + /** Gets whether the collection is trusted. */ + getTrust(collection: McpCollectionReference): IObservable; + + /** Resets any saved inputs for the connection. */ + clearSavedInputs(collection: McpCollectionReference, definition: McpServerDefinition): void; + /** Creates a connection for the collection and definition. */ + resolveConnection(options: IMcpResolveConnectionOptions): Promise; +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts new file mode 100644 index 00000000000..640f98da8ca --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -0,0 +1,325 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { raceCancellationError, Sequencer } from '../../../../base/common/async.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { LRUCache } from '../../../../base/common/map.js'; +import { autorun, autorunWithStore, derived, disposableObservableValue, IObservable, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { mcpActivationEvent } from './mcpConfiguration.js'; +import { IMcpRegistry } from './mcpRegistryTypes.js'; +import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; +import { extensionMcpCollectionPrefix, IMcpServer, IMcpServerConnection, IMcpTool, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, McpServerDefinition, McpServerToolsState } from './mcpTypes.js'; +import { MCP } from './modelContextProtocol.js'; + + +interface IToolCacheEntry { + /** Cached tools so we can show what's available before it's started */ + readonly tools: readonly MCP.Tool[]; +} + +interface IServerCacheEntry { + readonly servers: readonly McpServerDefinition.Serialized[]; +} + +export class McpServerMetadataCache extends Disposable { + private didChange = false; + private readonly cache = new LRUCache(128); + private readonly extensionServers = new Map(); + + constructor( + scope: StorageScope, + @IStorageService storageService: IStorageService, + ) { + super(); + + type StoredType = { + extensionServers: [string, IServerCacheEntry][]; + serverTools: [string, IToolCacheEntry][]; + }; + + const storageKey = 'mcpToolCache'; + this._register(storageService.onWillSaveState(() => { + if (this.didChange) { + storageService.store(storageKey, { + extensionServers: [...this.extensionServers], + serverTools: this.cache.toJSON(), + } satisfies StoredType, scope, StorageTarget.MACHINE); + this.didChange = false; + } + })); + + try { + const cached: StoredType | undefined = storageService.getObject(storageKey, scope); + this.extensionServers = new Map(cached?.extensionServers ?? []); + cached?.serverTools?.forEach(([k, v]) => this.cache.set(k, v)); + } catch { + // ignored + } + } + + /** Resets the cache for tools and extension servers */ + reset() { + this.cache.clear(); + this.extensionServers.clear(); + this.didChange = true; + } + + /** Gets cached tools for a server (used before a server is running) */ + getTools(definitionId: string): readonly MCP.Tool[] | undefined { + return this.cache.get(definitionId)?.tools; + } + + /** Sets cached tools for a server */ + storeTools(definitionId: string, tools: readonly MCP.Tool[]): void { + this.cache.set(definitionId, { ...this.cache.get(definitionId), tools }); + this.didChange = true; + } + + /** Gets cached servers for a collection (used for extensions, before the extension activates) */ + getServers(collectionId: string) { + return this.extensionServers.get(collectionId); + } + + /** Sets cached servers for a collection */ + storeServers(collectionId: string, entry: IServerCacheEntry | undefined): void { + if (entry) { + this.extensionServers.set(collectionId, entry); + } else { + this.extensionServers.delete(collectionId); + } + this.didChange = true; + } +} + +export class McpServer extends Disposable implements IMcpServer { + private readonly _connectionSequencer = new Sequencer(); + private readonly _connection = this._register(disposableObservableValue(this, undefined)); + + public readonly connection = this._connection; + public readonly connectionState: IObservable = derived(reader => this._connection.read(reader)?.state.read(reader) ?? { state: McpConnectionState.Kind.Stopped }); + + private get toolsFromCache() { + return this._toolCache.getTools(this.definition.id); + } + private readonly toolsFromServerPromise = observableValue | undefined>(this, undefined); + private readonly toolsFromServer = derived(reader => this.toolsFromServerPromise.read(reader)?.promiseResult.read(reader)?.data); + + public readonly tools: IObservable; + + public readonly toolsState = derived(reader => { + const fromServer = this.toolsFromServerPromise.read(reader); + const connectionState = this.connectionState.read(reader); + const isIdle = McpConnectionState.canBeStarted(connectionState.state) && !fromServer; + if (isIdle) { + return this.toolsFromCache ? McpServerToolsState.Cached : McpServerToolsState.Unknown; + } + + const fromServerResult = fromServer?.promiseResult.read(reader); + if (!fromServerResult) { + return this.toolsFromCache ? McpServerToolsState.RefreshingFromCached : McpServerToolsState.RefreshingFromUnknown; + } + + return fromServerResult.error ? (this.toolsFromCache ? McpServerToolsState.Cached : McpServerToolsState.Unknown) : McpServerToolsState.Live; + }); + + public get trusted() { + return this._mcpRegistry.getTrust(this.collection); + } + + constructor( + public readonly collection: McpCollectionReference, + public readonly definition: McpDefinitionReference, + private readonly _requiresExtensionActivation: boolean | undefined, + private readonly _toolCache: McpServerMetadataCache, + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + @IWorkspaceContextService workspacesService: IWorkspaceContextService, + @IExtensionService private readonly _extensionService: IExtensionService, + ) { + super(); + + // 1. Reflect workspaces into the MCP roots + const workspaces = observableFromEvent( + this, + workspacesService.onDidChangeWorkspaceFolders, + () => workspacesService.getWorkspace().folders, + ); + + this._register(autorunWithStore(reader => { + const cnx = this._connection.read(reader)?.handler.read(reader); + if (!cnx) { + return; + } + + cnx.roots = workspaces.read(reader).map(wf => ({ + uri: wf.uri.toString(), + name: wf.name, + })); + })); + + // 2. Populate this.tools when we connect to a server. + this._register(autorunWithStore((reader, store) => { + const cnx = this._connection.read(reader)?.handler.read(reader); + if (cnx) { + this.populateLiveData(cnx, store); + } else { + this.resetLiveData(); + } + })); + + // 3. Update the cache when tools update + this._register(autorun(reader => { + const tools = this.toolsFromServer.read(reader); + if (tools) { + this._toolCache.storeTools(definition.id, tools); + } + })); + + // 4. Publish tools + const toolPrefix = this._mcpRegistry.collectionToolPrefix(this.collection); + this.tools = derived(reader => { + const serverTools = this.toolsFromServer.read(reader); + const definitions = serverTools ?? this.toolsFromCache ?? []; + const prefix = toolPrefix.read(reader); + return definitions.map(def => new McpTool(this, prefix, def)); + }); + } + + public showOutput(): void { + this._connection.get()?.showOutput(); + } + + public start(isFromInteraction?: boolean): Promise { + return this._connectionSequencer.queue(async () => { + const activationEvent = mcpActivationEvent(this.collection.id.slice(extensionMcpCollectionPrefix.length)); + if (this._requiresExtensionActivation && !this._extensionService.activationEventIsDone(activationEvent)) { + await this._extensionService.activateByEvent(activationEvent); + await Promise.all(this._mcpRegistry.delegates + .map(r => r.waitForInitialProviderPromises())); + // This can happen if the server was created from a cached MCP server seen + // from an extension, but then it wasn't registered when the extension activated. + if (this._store.isDisposed) { + return { state: McpConnectionState.Kind.Stopped }; + } + } + + let connection = this._connection.get(); + if (connection && McpConnectionState.canBeStarted(connection.state.get().state)) { + connection.dispose(); + connection = undefined; + this._connection.set(connection, undefined); + } + + if (!connection) { + connection = await this._mcpRegistry.resolveConnection({ + collectionRef: this.collection, + definitionRef: this.definition, + forceTrust: isFromInteraction, + }); + if (!connection) { + return { state: McpConnectionState.Kind.Stopped }; + } + + if (this._store.isDisposed) { + connection.dispose(); + return { state: McpConnectionState.Kind.Stopped }; + } + + this._connection.set(connection, undefined); + } + + return connection.start(); + }); + } + + public stop(): Promise { + return this._connection.get()?.stop() || Promise.resolve(); + } + + private resetLiveData() { + transaction(tx => { + this.toolsFromServerPromise.set(undefined, tx); + }); + } + + private populateLiveData(handler: McpServerRequestHandler, store: DisposableStore) { + const cts = new CancellationTokenSource(); + store.add(toDisposable(() => cts.dispose(true))); + + // todo: add more than just tools here + + const updateTools = (tx: ITransaction | undefined) => { + this.toolsFromServerPromise.set(new ObservablePromise(handler.listTools({}, cts.token)), tx); + }; + + store.add(handler.onDidChangeToolList(() => updateTools(undefined))); + + transaction(tx => { + updateTools(tx); + }); + } + + /** + * Helper function to call the function on the handler once it's online. The + * connection started if it is not already. + */ + public async callOn(fn: (handler: McpServerRequestHandler) => Promise, token: CancellationToken = CancellationToken.None): Promise { + + await this.start(); // idempotent + + let ranOnce = false; + let d: IDisposable; + + const callPromise = new Promise((resolve, reject) => { + + d = autorun(reader => { + const connection = this._connection.read(reader); + if (!connection || ranOnce) { + return; + } + + const handler = connection.handler.read(reader); + if (!handler) { + const state = connection.state.read(reader); + if (state.state === McpConnectionState.Kind.Error) { + reject(new McpConnectionFailedError(`MCP server could not be started: ${state.message}`)); + return; + } else if (state.state === McpConnectionState.Kind.Stopped) { + reject(new McpConnectionFailedError('MCP server has stopped')); + return; + } else { + // keep waiting for handler + return; + } + } + + resolve(fn(handler)); + ranOnce = true; // aggressive prevent multiple racey calls, don't dispose because autorun is sync + }); + }); + + return raceCancellationError(callPromise, token).finally(() => d.dispose()); + } +} + +export class McpTool implements IMcpTool { + + readonly id: string; + + constructor( + private readonly _server: McpServer, + idPrefix: string, + public readonly definition: MCP.Tool, + ) { + this.id = (idPrefix + definition.name).replaceAll('.', '_'); + } + + call(params: Record, token?: CancellationToken): Promise { + return this._server.callOn(h => h.callTool({ name: this.definition.name, arguments: params }), token); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts new file mode 100644 index 00000000000..5016ebe7f53 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { Disposable, DisposableStore, IReference, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js'; +import { localize } from '../../../../nls.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; +import { IOutputService } from '../../../services/output/common/output.js'; +import { IMcpHostDelegate, IMcpMessageTransport } from './mcpRegistryTypes.js'; +import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; +import { McpCollectionDefinition, IMcpServerConnection, McpServerDefinition, McpConnectionState, McpServerLaunch } from './mcpTypes.js'; + +export class McpServerConnection extends Disposable implements IMcpServerConnection { + private readonly _launch = this._register(new MutableDisposable>()); + private readonly _state = observableValue('mcpServerState', { state: McpConnectionState.Kind.Stopped }); + private readonly _requestHandler = observableValue('mcpServerRequestHandler', undefined); + + public readonly state: IObservable = this._state; + public readonly handler: IObservable = this._requestHandler; + + private readonly _loggerId: string; + private readonly _logger: ILogger; + private _launchId = 0; + + constructor( + private readonly _collection: McpCollectionDefinition, + public readonly definition: McpServerDefinition, + private readonly _delegate: IMcpHostDelegate, + public readonly launchDefinition: McpServerLaunch, + @ILoggerService private readonly _loggerService: ILoggerService, + @IOutputService private readonly _outputService: IOutputService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + this._loggerId = `mcpServer/${definition.id}`; + this._logger = this._register(_loggerService.createLogger(this._loggerId, { hidden: true, name: `MCP: ${definition.label}` })); + } + + /** @inheritdoc */ + public showOutput(): void { + this._loggerService.setVisibility(this._loggerId, true); + this._outputService.showChannel(this._loggerId); + } + + /** @inheritdoc */ + public async start(): Promise { + const currentState = this._state.get(); + if (!McpConnectionState.canBeStarted(currentState.state)) { + return this._waitForState(McpConnectionState.Kind.Running, McpConnectionState.Kind.Error); + } + + const launchId = ++this._launchId; + this._launch.value = undefined; + this._state.set({ state: McpConnectionState.Kind.Starting }, undefined); + this._logger.info(localize('mcpServer.starting', 'Starting server {0}', this.definition.label)); + + try { + const launch = this._delegate.start(this._collection, this.definition, this.launchDefinition); + this._launch.value = this.adoptLaunch(launch, launchId); + return this._waitForState(McpConnectionState.Kind.Running, McpConnectionState.Kind.Error); + } catch (e) { + const errorState: McpConnectionState = { + state: McpConnectionState.Kind.Error, + message: e instanceof Error ? e.message : String(e) + }; + this._state.set(errorState, undefined); + return errorState; + } + } + + private adoptLaunch(launch: IMcpMessageTransport, launchId: number): IReference { + const store = new DisposableStore(); + const cts = new CancellationTokenSource(); + + store.add(toDisposable(() => cts.dispose(true))); + store.add(launch); + store.add(launch.onDidLog(msg => { + this._logger.info(msg); + })); + + let didStart = false; + store.add(autorun(reader => { + const state = launch.state.read(reader); + this._state.set(state, undefined); + this._logger.info(localize('mcpServer.state', 'Connection state: {0}', McpConnectionState.toString(state))); + + if (state.state === McpConnectionState.Kind.Running && !didStart) { + didStart = true; + McpServerRequestHandler.create(this._instantiationService, launch, this._logger, cts.token).then( + handler => { + if (this._launchId === launchId) { + this._requestHandler.set(handler, undefined); + } else { + handler.dispose(); + } + }, + err => { + store.dispose(); + if (this._launchId === launchId) { + this._logger.error(err); + this._state.set({ state: McpConnectionState.Kind.Error, message: `Could not initialize MCP server: ${err.message}` }, undefined); + } + }, + ); + } + })); + + return { dispose: () => store.dispose(), object: launch }; + } + + public async stop(): Promise { + this._launchId = -1; + this._logger.info(localize('mcpServer.stopping', 'Stopping server {0}', this.definition.label)); + this._launch.value?.object.stop(); + await this._waitForState(McpConnectionState.Kind.Stopped, McpConnectionState.Kind.Error); + } + + public override dispose(): void { + this._launchId = -1; + this._requestHandler.get()?.dispose(); + super.dispose(); + this._state.set({ state: McpConnectionState.Kind.Stopped }, undefined); + } + + private _waitForState(...kinds: McpConnectionState.Kind[]): Promise { + const current = this._state.get(); + if (kinds.includes(current.state)) { + return Promise.resolve(current); + } + + return new Promise(resolve => { + const disposable = autorun(reader => { + const state = this._state.read(reader); + if (kinds.includes(state.state)) { + disposable.dispose(); + resolve(state); + } + }); + }); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts new file mode 100644 index 00000000000..651cb01d8cd --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts @@ -0,0 +1,461 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equals } from '../../../../base/common/arrays.js'; +import { DeferredPromise } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogger } from '../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IMcpMessageTransport } from './mcpRegistryTypes.js'; +import { McpConnectionState, MpcResponseError } from './mcpTypes.js'; +import { MCP } from './modelContextProtocol.js'; + +/** + * Maps request IDs to handlers. + */ +interface PendingRequest { + promise: DeferredPromise; +} + +export interface McpRoot { + uri: string; + name?: string; +} + +/** + * Request handler for communicating with an MCP server. + * + * Handles sending requests and receiving responses, with automatic + * handling of ping requests and typed client request methods. + */ +export class McpServerRequestHandler extends Disposable { + private _nextRequestId = 1; + private readonly _pendingRequests = new Map(); + + private _hasAnnouncedRoots = false; + private _roots: MCP.Root[] = []; + + public set roots(roots: MCP.Root[]) { + if (!equals(this._roots, roots)) { + this._roots = roots; + if (this._hasAnnouncedRoots) { + this.sendNotification({ method: 'notifications/roots/list_changed' }); + this._hasAnnouncedRoots = false; + } + } + } + + private _serverInit!: MCP.InitializeResult; + public get capabilities(): MCP.ServerCapabilities { + return this._serverInit.capabilities; + } + + // Event emitters for server notifications + private readonly _onDidReceiveCancelledNotification = this._register(new Emitter()); + readonly onDidReceiveCancelledNotification = this._onDidReceiveCancelledNotification.event; + + private readonly _onDidReceiveProgressNotification = this._register(new Emitter()); + readonly onDidReceiveProgressNotification = this._onDidReceiveProgressNotification.event; + + private readonly _onDidChangeResourceList = this._register(new Emitter()); + readonly onDidChangeResourceList = this._onDidChangeResourceList.event; + + private readonly _onDidUpdateResource = this._register(new Emitter()); + readonly onDidUpdateResource = this._onDidUpdateResource.event; + + private readonly _onDidChangeToolList = this._register(new Emitter()); + readonly onDidChangeToolList = this._onDidChangeToolList.event; + + private readonly _onDidChangePromptList = this._register(new Emitter()); + readonly onDidChangePromptList = this._onDidChangePromptList.event; + + /** + * Connects to the MCP server and does the initialization handshake. + * @throws MpcResponseError if the server fails to initialize. + */ + public static async create(instaService: IInstantiationService, launch: IMcpMessageTransport, logger: ILogger, token?: CancellationToken) { + const mcp = new McpServerRequestHandler(launch, logger); + try { + await instaService.invokeFunction(async accessor => { + const productService = accessor.get(IProductService); + const initialized = await mcp.sendRequest({ + method: 'initialize', + params: { + protocolVersion: MCP.LATEST_PROTOCOL_VERSION, + capabilities: { + roots: { listChanged: true }, + }, + clientInfo: { + name: productService.nameLong, + version: productService.version, + } + } + }, token); + + mcp._serverInit = initialized; + + mcp.sendNotification({ + method: 'notifications/initialized' + }); + }); + + return mcp; + } catch (e) { + mcp.dispose(); + throw e; + } + } + + protected constructor( + private readonly launch: IMcpMessageTransport, + private readonly logger: ILogger, + ) { + super(); + this._register(launch.onDidReceiveMessage(message => this.handleMessage(message))); + this._register(autorun(reader => { + const state = launch.state.read(reader).state; + // the handler will get disposed when the launch stops, but if we're still + // create()'ing we need to make sure to cancel the initialize request. + if (state === McpConnectionState.Kind.Error || state === McpConnectionState.Kind.Stopped) { + this.cancelAllRequests(); + } + })); + } + + /** + * Send a client request to the server and return the response. + * + * @param request The request to send + * @param token Cancellation token + * @param timeoutMs Optional timeout in milliseconds + * @returns A promise that resolves with the response + */ + private async sendRequest( + request: Pick, + token: CancellationToken = CancellationToken.None + ): Promise { + const id = this._nextRequestId++; + + // Create the full JSON-RPC request + const jsonRpcRequest: MCP.JSONRPCRequest = { + jsonrpc: MCP.JSONRPC_VERSION, + id, + ...request + }; + + const promise = new DeferredPromise(); + // Store the pending request + this._pendingRequests.set(id, { promise }); + + // Set up cancellation + const cancelListener = token.onCancellationRequested(() => { + if (!promise.isSettled) { + this._pendingRequests.delete(id); + this.sendNotification({ method: 'notifications/cancelled', params: { requestId: id } }); + promise.cancel(); + } + }); + + // Send the request + this.launch.send(jsonRpcRequest); + const ret = promise.p.finally(() => { + cancelListener.dispose(); + this._pendingRequests.delete(id); + }); + + return ret as Promise; + } + + /** + * Handles paginated requests by making multiple requests until all items are retrieved. + * + * @param method The method name to call + * @param getItems Function to extract the array of items from a result + * @param initialParams Initial parameters + * @param token Cancellation token + * @returns Promise with all items combined + */ + private async sendRequestPaginated(method: T['method'], getItems: (result: R) => I[], initialParams?: Omit, token: CancellationToken = CancellationToken.None): Promise { + let allItems: I[] = []; + let nextCursor: MCP.Cursor | undefined = undefined; + + do { + const params: T['params'] = { + ...initialParams, + cursor: nextCursor + }; + + const result: R = await this.sendRequest({ method, params }, token); + allItems = allItems.concat(getItems(result)); + nextCursor = result.nextCursor; + } while (nextCursor !== undefined && !token.isCancellationRequested); + + return allItems; + } + + private sendNotification(notification: N): void { + this.launch.send({ ...notification, jsonrpc: MCP.JSONRPC_VERSION }); + } + + /** + * Handle incoming messages from the server + */ + private handleMessage(message: MCP.JSONRPCMessage): void { + // Handle responses to our requests + if ('id' in message) { + if ('result' in message) { + this.handleResult(message); + } else if ('error' in message) { + this.handleError(message); + } + } + + // Handle requests from the server + if ('method' in message) { + if ('id' in message) { + this.handleServerRequest(message as MCP.JSONRPCRequest & MCP.ServerRequest); + } else { + this.handleServerNotification(message as MCP.JSONRPCNotification & MCP.ServerNotification); + + } + } + } + + /** + * Handle successful responses + */ + private handleResult(response: MCP.JSONRPCResponse): void { + const request = this._pendingRequests.get(response.id); + if (request) { + this._pendingRequests.delete(response.id); + request.promise.complete(response.result); + } + } + + /** + * Handle error responses + */ + private handleError(response: MCP.JSONRPCError): void { + const request = this._pendingRequests.get(response.id); + if (request) { + this._pendingRequests.delete(response.id); + request.promise.error(new MpcResponseError(response.error.message, response.error.code, response.error.data)); + } + } + + /** + * Handle incoming server requests + */ + private handleServerRequest(request: MCP.JSONRPCRequest & MCP.ServerRequest): void { + switch (request.method) { + case 'ping': + return this.respondToRequest(request, this.handlePing(request)); + case 'roots/list': + return this.respondToRequest(request, this.handleRootsList(request)); + + default: { + const errorResponse: MCP.JSONRPCError = { + jsonrpc: MCP.JSONRPC_VERSION, + id: request.id, + error: { + code: MCP.METHOD_NOT_FOUND, + message: `Method not found: ${request.method}` + } + }; + this.launch.send(errorResponse); + break; + } + } + } + /** + * Handle incoming server notifications + */ + private handleServerNotification(request: MCP.JSONRPCNotification & MCP.ServerNotification): void { + switch (request.method) { + case 'notifications/message': + return this.handleLoggingNotification(request); + case 'notifications/cancelled': + this._onDidReceiveCancelledNotification.fire(request); + return this.handleCancelledNotification(request); + case 'notifications/progress': + this._onDidReceiveProgressNotification.fire(request); + return; + case 'notifications/resources/list_changed': + this._onDidChangeResourceList.fire(); + return; + case 'notifications/resources/updated': + this._onDidUpdateResource.fire(request); + return; + case 'notifications/tools/list_changed': + this._onDidChangeToolList.fire(); + return; + case 'notifications/prompts/list_changed': + this._onDidChangePromptList.fire(); + return; + } + } + + private handleCancelledNotification(request: MCP.CancelledNotification): void { + const pendingRequest = this._pendingRequests.get(request.params.requestId); + if (pendingRequest) { + this._pendingRequests.delete(request.params.requestId); + pendingRequest.promise.cancel(); + } + } + + private handleLoggingNotification(request: MCP.LoggingMessageNotification): void { + let contents = typeof request.params.data === 'string' ? request.params.data : JSON.stringify(request.params.data); + if (request.params.logger) { + contents = `${request.params.logger}: ${contents}`; + } + + switch (request.params?.level) { + case 'debug': + this.logger.debug(contents); + break; + case 'info': + case 'notice': + this.logger.info(contents); + break; + case 'warning': + this.logger.warn(contents); + break; + case 'error': + case 'critical': + case 'alert': + case 'emergency': + this.logger.error(contents); + break; + default: + this.logger.info(contents); + break; + } + } + + /** + * Send a generic response to a request + */ + private respondToRequest(request: MCP.JSONRPCRequest, result: MCP.Result): void { + const response: MCP.JSONRPCResponse = { + jsonrpc: MCP.JSONRPC_VERSION, + id: request.id, + result + }; + this.launch.send(response); + } + + /** + * Send a response to a ping request + */ + private handlePing(_request: MCP.PingRequest): {} { + return {}; + } + + /** + * Send a response to a roots/list request + */ + private handleRootsList(_request: MCP.ListRootsRequest): MCP.ListRootsResult { + this._hasAnnouncedRoots = true; + return { roots: this._roots }; + } + + private cancelAllRequests() { + this._pendingRequests.forEach(pending => pending.promise.cancel()); + this._pendingRequests.clear(); + } + + public override dispose(): void { + this.cancelAllRequests(); + super.dispose(); + } + + /** + * Send an initialize request + */ + initialize(params: MCP.InitializeRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'initialize', params }, token); + } + + /** + * List available resources + */ + listResources(params?: MCP.ListResourcesRequest['params'], token?: CancellationToken): Promise { + return this.sendRequestPaginated('resources/list', result => result.resources, params, token); + } + + /** + * Read a specific resource + */ + readResource(params: MCP.ReadResourceRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'resources/read', params }, token); + } + + /** + * List available resource templates + */ + listResourceTemplates(params?: MCP.ListResourceTemplatesRequest['params'], token?: CancellationToken): Promise { + return this.sendRequestPaginated('resources/templates/list', result => result.resourceTemplates, params, token); + } + + /** + * Subscribe to resource updates + */ + subscribe(params: MCP.SubscribeRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'resources/subscribe', params }, token); + } + + /** + * Unsubscribe from resource updates + */ + unsubscribe(params: MCP.UnsubscribeRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'resources/unsubscribe', params }, token); + } + + /** + * List available prompts + */ + listPrompts(params?: MCP.ListPromptsRequest['params'], token?: CancellationToken): Promise { + return this.sendRequestPaginated('prompts/list', result => result.prompts, params, token); + } + + /** + * Get a specific prompt + */ + getPrompt(params: MCP.GetPromptRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'prompts/get', params }, token); + } + + /** + * List available tools + */ + listTools(params?: MCP.ListToolsRequest['params'], token?: CancellationToken): Promise { + return this.sendRequestPaginated('tools/list', result => result.tools, params, token); + } + + /** + * Call a specific tool + */ + callTool(params: MCP.CallToolRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'tools/call', params }, token); + } + + /** + * Set the logging level + */ + setLevel(params: MCP.SetLevelRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'logging/setLevel', params }, token); + } + + /** + * Find completions for an argument + */ + complete(params: MCP.CompleteRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'completion/complete', params }, token); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts new file mode 100644 index 00000000000..5845db90584 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -0,0 +1,249 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; +import { equals } from '../../../../base/common/objects.js'; +import { autorun, IObservable, observableValue, transaction } from '../../../../base/common/observable.js'; +import { localize } from '../../../../nls.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { StorageScope } from '../../../../platform/storage/common/storage.js'; +import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; +import { IMcpRegistry } from './mcpRegistryTypes.js'; +import { McpServer, McpServerMetadataCache } from './mcpServer.js'; +import { IMcpServer, IMcpService, IMcpTool, McpCollectionDefinition, McpServerDefinition, McpServerToolsState } from './mcpTypes.js'; + +interface ISyncedToolData { + toolData: IToolData; + toolDispose: IDisposable; + implDispose: IDisposable; +} + +type IMcpServerRec = IReference; + +export class McpService extends Disposable implements IMcpService { + + declare _serviceBrand: undefined; + + private readonly _servers = observableValue(this, []); + public readonly servers: IObservable = this._servers.map(servers => servers.map(s => s.object)); + + public get lazyCollectionState() { return this._mcpRegistry.lazyCollectionState; } + + protected readonly userCache: McpServerMetadataCache; + protected readonly workspaceCache: McpServerMetadataCache; + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + @ILanguageModelToolsService private readonly _toolsService: ILanguageModelToolsService, + @IProductService productService: IProductService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + + this.userCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.PROFILE)); + this.workspaceCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.WORKSPACE)); + + const updateThrottle = this._store.add(new RunOnceScheduler(() => this._updateCollectedServers(), 500)); + + // Throttle changes so that if a collection is changed, or a server is + // unregistered/registered, we don't stop servers unnecessarily. + this._register(autorun(reader => { + for (const collection of this._mcpRegistry.collections.read(reader)) { + collection.serverDefinitions.read(reader); + } + updateThrottle.schedule(500); + })); + } + + public resetCaches(): void { + this.userCache.reset(); + this.workspaceCache.reset(); + } + + public async activateCollections(): Promise { + const collections = await this._mcpRegistry.discoverCollections(); + const collectionIds = new Set(collections.map(c => c.id)); + + this._updateCollectedServers(); + + // Discover any newly-collected servers with unknown tools + const todo: Promise[] = []; + for (const { object: server } of this._servers.get()) { + if (collectionIds.has(server.collection.id)) { + const state = server.toolsState.get(); + if (state === McpServerToolsState.Unknown) { + todo.push(server.start()); + } + } + } + + await Promise.all(todo); + } + + private _syncTools(server: McpServer, store: DisposableStore) { + const tools = new Map(); + + store.add(autorun(reader => { + const toDelete = new Set(tools.keys()); + for (const tool of server.tools.read(reader)) { + const existing = tools.get(tool.id); + const toolData: IToolData = { + id: tool.id, + displayName: tool.definition.name, + toolReferenceName: tool.definition.name, + modelDescription: tool.definition.description ?? '', + userDescription: tool.definition.description ?? '', + inputSchema: tool.definition.inputSchema, + canBeReferencedInPrompt: true, + tags: ['mcp', 'vscode_editing'], // TODO@jrieken remove this tag + }; + + if (existing) { + if (!equals(existing.toolData, toolData)) { + existing.toolData = toolData; + existing.toolDispose.dispose(); + existing.toolDispose = this._toolsService.registerToolData(toolData); + } + toDelete.delete(tool.id); + } else { + tools.set(tool.id, { + toolData, + toolDispose: this._toolsService.registerToolData(toolData), + implDispose: this._toolsService.registerToolImplementation(tool.id, this._instantiationService.createInstance(McpToolImplementation, tool, server)), + }); + } + } + + for (const id of toDelete) { + const tool = tools.get(id); + if (tool) { + tool.toolDispose.dispose(); + tool.implDispose.dispose(); + tools.delete(id); + } + } + })); + + store.add(toDisposable(() => { + for (const tool of tools.values()) { + tool.toolDispose.dispose(); + tool.implDispose.dispose(); + } + })); + } + + private _updateCollectedServers() { + const definitions = this._mcpRegistry.collections.get().flatMap(collectionDefinition => + collectionDefinition.serverDefinitions.get().map(serverDefinition => ({ + serverDefinition, + collectionDefinition, + })) + ); + + const nextDefinitions = new Set(definitions); + const currentServers = this._servers.get(); + const nextServers: IMcpServerRec[] = []; + const pushMatch = (match: (typeof definitions)[0], rec: IMcpServerRec) => { + nextDefinitions.delete(match); + nextServers.push(rec); + const connection = rec.object.connection.get(); + // if the definition was modified, stop the server; it'll be restarted again on-demand + if (connection && !McpServerDefinition.equals(connection.definition, match.serverDefinition)) { + rec.object.stop(); + this._logService.debug(`MCP server ${rec.object.definition.id} stopped because the definition changed`); + } + }; + + // Transfer over any servers that are still valid. + for (const server of currentServers) { + const match = definitions.find(d => defsEqual(server.object, d)); + if (match) { + pushMatch(match, server); + } else { + server.dispose(); + } + } + + // Create any new servers that are needed. + for (const def of nextDefinitions) { + const store = new DisposableStore(); + const object = this._instantiationService.createInstance(McpServer, def.collectionDefinition, def.serverDefinition, false, def.collectionDefinition.scope === StorageScope.WORKSPACE ? this.workspaceCache : this.userCache); + store.add(object); + this._syncTools(object, store); + + nextServers.push({ object, dispose: () => store.dispose() }); + } + + transaction(tx => { + this._servers.set(nextServers, tx); + }); + } + + public override dispose(): void { + this._servers.get().forEach(s => s.dispose()); + super.dispose(); + } +} + +function defsEqual(server: IMcpServer, def: { serverDefinition: McpServerDefinition; collectionDefinition: McpCollectionDefinition }) { + return server.collection.id === def.collectionDefinition.id && server.definition.id === def.serverDefinition.id; +} + +class McpToolImplementation implements IToolImpl { + constructor( + private readonly _tool: IMcpTool, + private readonly _server: IMcpServer, + @IProductService private readonly _productService: IProductService, + ) { } + + async prepareToolInvocation(parameters: any, token: CancellationToken) { + const tool = this._tool; + const server = this._server; + + const mcpToolWarning = localize( + 'mcp.tool.warning', + "MCP servers or malicious conversation content may attempt to misuse '{0}' through the installed tools. Please carefully review any requested actions.", + this._productService.nameShort + ); + + return { + confirmationMessages: { + title: localize('msg.title', "Run `{0}` from $(server) `{1}` (MCP server)", tool.definition.name, server.definition.label), + message: new MarkdownString(localize('msg.msg', "{0}\n\nInput:\n\n```json\n{1}\n```\n\n$(warning) {2}", tool.definition.description, JSON.stringify(parameters, undefined, 2), mcpToolWarning), { supportThemeIcons: true }) + }, + invocationMessage: new MarkdownString(localize('msg.run', "Running `{0}`", tool.definition.name, server.definition.label)), + pastTenseMessage: new MarkdownString(localize('msg.ran', "Ran `{0}` ", tool.definition.name, server.definition.label)) + }; + } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, token: CancellationToken) { + + const result: IToolResult = { + content: [] + }; + + const callResult = await this._tool.call(invocation.parameters as Record, token); + for (const item of callResult.content) { + if (item.type === 'text') { + result.content.push({ + kind: 'text', + value: item.text + }); + } else { + // TODO@jrieken handle different item types + } + } + + // result.toolResultMessage = new MarkdownString(localize('reuslt.pattern', "```json\n{0}\n```", JSON.stringify(callResult, undefined, 2))); + + return result; + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts new file mode 100644 index 00000000000..db12df70ab6 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -0,0 +1,384 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assertNever } from '../../../../base/common/assert.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { equals as objectsEqual } from '../../../../base/common/objects.js'; +import { IObservable } from '../../../../base/common/observable.js'; +import { URI, UriComponents } from '../../../../base/common/uri.js'; +import { localize } from '../../../../nls.js'; +import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { StorageScope } from '../../../../platform/storage/common/storage.js'; +import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; +import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; +import { MCP } from './modelContextProtocol.js'; + +export const extensionMcpCollectionPrefix = 'ext.'; + +export function extensionPrefixedIdentifier(identifier: ExtensionIdentifier, id: string): string { + return ExtensionIdentifier.toKey(identifier) + '/' + id; +} + +/** + * An McpCollection contains McpServers. There may be multiple collections for + * different locations servers are discovered. + */ +export interface McpCollectionDefinition { + /** Origin authority from which this collection was discovered. */ + readonly remoteAuthority: string | null; + /** Globally-unique, stable ID for this definition */ + readonly id: string; + /** Human-readable label for the definition */ + readonly label: string; + /** Definitions this collection contains. */ + readonly serverDefinitions: IObservable; + /** If 'false', consent is required before any MCP servers in this collection are automatically launched. */ + readonly isTrustedByDefault: boolean; + /** Scope where associated collection info should be stored. */ + readonly scope: StorageScope; + + /** For lazy-loaded collections only: */ + readonly lazy?: { + /** True if `serverDefinitions` were loaded from the cache */ + isCached: boolean; + /** Triggers a load of the real server definition, which should be pushed to the IMcpRegistry. If not this definition will be removed. */ + load(): Promise; + /** Called after `load()` if the extension is not found. */ + removed?(): void; + }; + + readonly presentation?: { + /** Sort order of the collection. */ + readonly order?: number; + /** Place where this server is configured, used in workspac trust prompts */ + readonly origin?: URI; + }; +} + +export const enum McpCollectionSortOrder { + Workspace = 0, + User = 100, + Extension = 200, + Filesystem = 300, + + RemotePenalty = 50, +} + +export namespace McpCollectionDefinition { + export interface FromExtHost { + readonly id: string; + readonly label: string; + readonly isTrustedByDefault: boolean; + readonly scope: StorageScope; + } + + export function equals(a: McpCollectionDefinition, b: McpCollectionDefinition): boolean { + return a.id === b.id + && a.remoteAuthority === b.remoteAuthority + && a.label === b.label + && a.isTrustedByDefault === b.isTrustedByDefault; + } +} + +export interface McpServerDefinition { + /** Globally-unique, stable ID for this definition */ + readonly id: string; + /** Human-readable label for the definition */ + readonly label: string; + /** Descriptor defining how the configuration should be launched. */ + readonly launch: McpServerLaunch; + /** If set, allows configuration variables to be resolved in the {@link launch} with the given context */ + readonly variableReplacement?: McpServerDefinitionVariableReplacement; +} + +export namespace McpServerDefinition { + export interface Serialized { + readonly id: string; + readonly label: string; + readonly launch: McpServerLaunch.Serialized; + readonly variableReplacement?: McpServerDefinitionVariableReplacement.Serialized; + } + + export function toSerialized(def: McpServerDefinition): McpServerDefinition.Serialized { + return def; + } + + export function fromSerialized(def: McpServerDefinition.Serialized): McpServerDefinition { + return { + id: def.id, + label: def.label, + launch: McpServerLaunch.fromSerialized(def.launch), + variableReplacement: def.variableReplacement ? McpServerDefinitionVariableReplacement.fromSerialized(def.variableReplacement) : undefined, + }; + } + + export function equals(a: McpServerDefinition, b: McpServerDefinition): boolean { + return a.id === b.id + && a.label === b.label + && objectsEqual(a.launch, b.launch) + && objectsEqual(a.variableReplacement, b.variableReplacement); + } +} + + +export interface McpServerDefinitionVariableReplacement { + section?: string; // e.g. 'mcp' + folder?: IWorkspaceFolderData; + target?: ConfigurationTarget; +} + +export namespace McpServerDefinitionVariableReplacement { + export interface Serialized { + section?: string; + folder?: { name: string; index: number; uri: UriComponents }; + target?: ConfigurationTarget; + } + + export function toSerialized(def: McpServerDefinitionVariableReplacement): McpServerDefinitionVariableReplacement.Serialized { + return def; + } + + export function fromSerialized(def: McpServerDefinitionVariableReplacement.Serialized): McpServerDefinitionVariableReplacement { + return { + section: def.section, + folder: def.folder ? { ...def.folder, uri: URI.revive(def.folder.uri) } : undefined, + target: def.target, + }; + } +} + +export interface IMcpService { + _serviceBrand: undefined; + readonly servers: IObservable; + + /** Resets the cached tools. */ + resetCaches(): void; + + /** Set if there are extensions that register MCP servers that have never been activated. */ + readonly lazyCollectionState: IObservable; + /** Activatese extensions and runs their MCP servers. */ + activateCollections(): Promise; +} + +export const enum LazyCollectionState { + HasUnknown, + LoadingUnknown, + AllKnown, +} + +export const IMcpService = createDecorator('IMcpService'); + +export interface McpCollectionReference { + id: string; + label: string; + presentation?: McpCollectionDefinition['presentation']; +} + +export interface McpDefinitionReference { + id: string; + label: string; +} + +export interface IMcpServer extends IDisposable { + readonly collection: McpCollectionReference; + readonly definition: McpDefinitionReference; + readonly connection: IObservable; + readonly connectionState: IObservable; + /** + * Reflects the MCP server trust state. True if trusted, false if untrusted, + * undefined if consent is required but not indicated. + */ + readonly trusted: IObservable; + + showOutput(): void; + /** + * Starts the server and returns its resulting state. One of: + * - Running, if all good + * - Error, if the server failed to start + * - Stopped, if the server was disposed or the user cancelled the launch + */ + start(isFromInteraction?: boolean): Promise; + stop(): Promise; + + readonly toolsState: IObservable; + readonly tools: IObservable; +} + +export const enum McpServerToolsState { + /** Tools have not been read before */ + Unknown, + /** Tools were read from the cache */ + Cached, + /** Tools are refreshing for the first time */ + RefreshingFromUnknown, + /** Tools are refreshing and the current tools are cached */ + RefreshingFromCached, + /** Tool state is live, server is connected */ + Live, +} + +export interface IMcpTool { + + readonly id: string; + + readonly definition: MCP.Tool; + + /** + * Calls a tool + * @throws {@link MpcResponseError} if the tool fails to execute + * @throws {@link McpConnectionFailedError} if the connection to the server fails + */ + call(params: Record, token?: CancellationToken): Promise; +} + +export const enum McpServerTransportType { + /** A command-line MCP server communicating over standard in/out */ + Stdio = 1 << 0, + /** An MCP server that uses Server-Sent Events */ + SSE = 1 << 1, +} + +/** + * MCP server launched on the command line which communicated over stdio. + * https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio + */ +export interface McpServerTransportStdio { + readonly type: McpServerTransportType.Stdio; + readonly cwd: URI | undefined; + readonly command: string; + readonly args: readonly string[]; + readonly env: Record; +} + +/** + * MCP server launched on the command line which communicated over server-sent-events. + * https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse + */ +export interface McpServerTransportSSE { + readonly type: McpServerTransportType.SSE; + readonly uri: URI; + readonly headers: [string, string][]; +} + +export type McpServerLaunch = + | McpServerTransportStdio + | McpServerTransportSSE; + +export namespace McpServerLaunch { + export type Serialized = + | { type: McpServerTransportType.SSE; uri: UriComponents; headers: [string, string][] } + | { type: McpServerTransportType.Stdio; cwd: UriComponents | undefined; command: string; args: readonly string[]; env: Record }; + + export function toSerialized(launch: McpServerLaunch): McpServerLaunch.Serialized { + return launch; + } + + export function fromSerialized(launch: McpServerLaunch.Serialized): McpServerLaunch { + switch (launch.type) { + case McpServerTransportType.SSE: + return { type: launch.type, uri: URI.revive(launch.uri), headers: launch.headers }; + case McpServerTransportType.Stdio: + return { + type: launch.type, + cwd: launch.cwd ? URI.revive(launch.cwd) : undefined, + command: launch.command, + args: launch.args, + env: launch.env, + }; + } + } +} + +/** + * An instance that manages a connection to an MCP server. It can be started, + * stopped, and restarted. Once started and in a running state, it will + * eventually build a {@link IMcpServerConnection.handler}. + */ +export interface IMcpServerConnection extends IDisposable { + readonly definition: McpServerDefinition; + readonly state: IObservable; + readonly handler: IObservable; + + /** + * Shows the current server output. + */ + showOutput(): void; + + /** + * Starts the server if it's stopped. Returns a promise that resolves once + * server exits a 'starting' state. + */ + start(): Promise; + + /** + * Stops the server. + */ + stop(): Promise; +} + +/** + * McpConnectionState is the state of the underlying connection and is + * communicated e.g. from the extension host to the renderer. + */ +export namespace McpConnectionState { + export const enum Kind { + Stopped, + Starting, + Running, + Error, + } + + export const toString = (s: McpConnectionState): string => { + switch (s.state) { + case Kind.Stopped: + return localize('mcpstate.stopped', 'Stopped'); + case Kind.Starting: + return localize('mcpstate.starting', 'Starting'); + case Kind.Running: + return localize('mcpstate.running', 'Running'); + case Kind.Error: + return localize('mcpstate.error', 'Error {0}', s.message); + default: + assertNever(s); + } + }; + + /** Returns if the MCP state is one where starting a new server is valid */ + export const canBeStarted = (s: Kind) => s === Kind.Error || s === Kind.Stopped; + + export interface Stopped { + readonly state: Kind.Stopped; + } + + export interface Starting { + readonly state: Kind.Starting; + } + + export interface Running { + readonly state: Kind.Running; + } + + export interface Error { + readonly state: Kind.Error; + readonly message: string; + } +} + +export type McpConnectionState = + | McpConnectionState.Stopped + | McpConnectionState.Starting + | McpConnectionState.Running + | McpConnectionState.Error; + +export class MpcResponseError extends Error { + constructor(message: string, public readonly code: number, public readonly data: unknown) { + super(`MPC ${code}: ${message}`); + } +} + +export class McpConnectionFailedError extends Error { } diff --git a/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts new file mode 100644 index 00000000000..582e69eaf93 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts @@ -0,0 +1,1138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* eslint-disable @stylistic/ts/member-delimiter-style */ +/* eslint-disable local/code-no-unexternalized-strings */ + +/** + * Schema updated from the Model Context Protocol repository at + * https://github.com/modelcontextprotocol/specification/tree/main/schema + * + * ⚠️ Do not edit within `namespace` manually except to update schema versions ⚠️ + */ +export namespace MCP { + /* JSON-RPC types */ + export type JSONRPCMessage = + | JSONRPCRequest + | JSONRPCNotification + | JSONRPCResponse + | JSONRPCError; + + export const LATEST_PROTOCOL_VERSION = "2024-11-05"; + export const JSONRPC_VERSION = "2.0"; + + /** + * A progress token, used to associate progress notifications with the original request. + */ + export type ProgressToken = string | number; + + /** + * An opaque token used to represent a cursor for pagination. + */ + export type Cursor = string; + + export interface Request { + method: string; + params?: { + _meta?: { + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken?: ProgressToken; + }; + [key: string]: unknown; + }; + } + + export interface Notification { + method: string; + params?: { + /** + * This parameter name is reserved by MCP to allow clients and servers to attach additional metadata to their notifications. + */ + _meta?: { [key: string]: unknown }; + [key: string]: unknown; + }; + } + + export interface Result { + /** + * This result property is reserved by the protocol to allow clients and servers to attach additional metadata to their responses. + */ + _meta?: { [key: string]: unknown }; + [key: string]: unknown; + } + + /** + * A uniquely identifying ID for a request in JSON-RPC. + */ + export type RequestId = string | number; + + /** + * A request that expects a response. + */ + export interface JSONRPCRequest extends Request { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; + } + + /** + * A notification which does not expect a response. + */ + export interface JSONRPCNotification extends Notification { + jsonrpc: typeof JSONRPC_VERSION; + } + + /** + * A successful (non-error) response to a request. + */ + export interface JSONRPCResponse { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; + result: Result; + } + + // Standard JSON-RPC error codes + export const PARSE_ERROR = -32700; + export const INVALID_REQUEST = -32600; + export const METHOD_NOT_FOUND = -32601; + export const INVALID_PARAMS = -32602; + export const INTERNAL_ERROR = -32603; + + /** + * A response to a request that indicates an error occurred. + */ + export interface JSONRPCError { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; + error: { + /** + * The error type that occurred. + */ + code: number; + /** + * A short description of the error. The message SHOULD be limited to a concise single sentence. + */ + message: string; + /** + * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + */ + data?: unknown; + }; + } + + /* Empty result */ + /** + * A response that indicates success but carries no data. + */ + export type EmptyResult = Result; + + /* Cancellation */ + /** + * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. + * + * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. + * + * This notification indicates that the result will be unused, so any associated processing SHOULD cease. + * + * A client MUST NOT attempt to cancel its `initialize` request. + */ + export interface CancelledNotification extends Notification { + method: "notifications/cancelled"; + params: { + /** + * The ID of the request to cancel. + * + * This MUST correspond to the ID of a request previously issued in the same direction. + */ + requestId: RequestId; + + /** + * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. + */ + reason?: string; + }; + } + + /* Initialization */ + /** + * This request is sent from the client to the server when it first connects, asking it to begin initialization. + */ + export interface InitializeRequest extends Request { + method: "initialize"; + params: { + /** + * The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well. + */ + protocolVersion: string; + capabilities: ClientCapabilities; + clientInfo: Implementation; + }; + } + + /** + * After receiving an initialize request from the client, the server sends this response. + */ + export interface InitializeResult extends Result { + /** + * The version of the Model Context Protocol that the server wants to use. This may not match the version that the client requested. If the client cannot support this version, it MUST disconnect. + */ + protocolVersion: string; + capabilities: ServerCapabilities; + serverInfo: Implementation; + /** + * Instructions describing how to use the server and its features. + * + * This can be used by clients to improve the LLM's understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt. + */ + instructions?: string; + } + + /** + * This notification is sent from the client to the server after initialization has finished. + */ + export interface InitializedNotification extends Notification { + method: "notifications/initialized"; + } + + /** + * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + */ + export interface ClientCapabilities { + /** + * Experimental, non-standard capabilities that the client supports. + */ + experimental?: { [key: string]: object }; + /** + * Present if the client supports listing roots. + */ + roots?: { + /** + * Whether the client supports notifications for changes to the roots list. + */ + listChanged?: boolean; + }; + /** + * Present if the client supports sampling from an LLM. + */ + sampling?: object; + } + + /** + * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. + */ + export interface ServerCapabilities { + /** + * Experimental, non-standard capabilities that the server supports. + */ + experimental?: { [key: string]: object }; + /** + * Present if the server supports sending log messages to the client. + */ + logging?: object; + /** + * Present if the server offers any prompt templates. + */ + prompts?: { + /** + * Whether this server supports notifications for changes to the prompt list. + */ + listChanged?: boolean; + }; + /** + * Present if the server offers any resources to read. + */ + resources?: { + /** + * Whether this server supports subscribing to resource updates. + */ + subscribe?: boolean; + /** + * Whether this server supports notifications for changes to the resource list. + */ + listChanged?: boolean; + }; + /** + * Present if the server offers any tools to call. + */ + tools?: { + /** + * Whether this server supports notifications for changes to the tool list. + */ + listChanged?: boolean; + }; + } + + /** + * Describes the name and version of an MCP implementation. + */ + export interface Implementation { + name: string; + version: string; + } + + /* Ping */ + /** + * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. + */ + export interface PingRequest extends Request { + method: "ping"; + } + + /* Progress notifications */ + /** + * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + */ + export interface ProgressNotification extends Notification { + method: "notifications/progress"; + params: { + /** + * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. + */ + progressToken: ProgressToken; + /** + * The progress thus far. This should increase every time progress is made, even if the total is unknown. + * + * @TJS-type number + */ + progress: number; + /** + * Total number of items to process (or total progress required), if known. + * + * @TJS-type number + */ + total?: number; + }; + } + + /* Pagination */ + export interface PaginatedRequest extends Request { + params?: { + /** + * An opaque token representing the current pagination position. + * If provided, the server should return results starting after this cursor. + */ + cursor?: Cursor; + }; + } + + export interface PaginatedResult extends Result { + /** + * An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. + */ + nextCursor?: Cursor; + } + + /* Resources */ + /** + * Sent from the client to request a list of resources the server has. + */ + export interface ListResourcesRequest extends PaginatedRequest { + method: "resources/list"; + } + + /** + * The server's response to a resources/list request from the client. + */ + export interface ListResourcesResult extends PaginatedResult { + resources: Resource[]; + } + + /** + * Sent from the client to request a list of resource templates the server has. + */ + export interface ListResourceTemplatesRequest extends PaginatedRequest { + method: "resources/templates/list"; + } + + /** + * The server's response to a resources/templates/list request from the client. + */ + export interface ListResourceTemplatesResult extends PaginatedResult { + resourceTemplates: ResourceTemplate[]; + } + + /** + * Sent from the client to the server, to read a specific resource URI. + */ + export interface ReadResourceRequest extends Request { + method: "resources/read"; + params: { + /** + * The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it. + * + * @format uri + */ + uri: string; + }; + } + + /** + * The server's response to a resources/read request from the client. + */ + export interface ReadResourceResult extends Result { + contents: (TextResourceContents | BlobResourceContents)[]; + } + + /** + * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. + */ + export interface ResourceListChangedNotification extends Notification { + method: "notifications/resources/list_changed"; + } + + /** + * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. + */ + export interface SubscribeRequest extends Request { + method: "resources/subscribe"; + params: { + /** + * The URI of the resource to subscribe to. The URI can use any protocol; it is up to the server how to interpret it. + * + * @format uri + */ + uri: string; + }; + } + + /** + * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. + */ + export interface UnsubscribeRequest extends Request { + method: "resources/unsubscribe"; + params: { + /** + * The URI of the resource to unsubscribe from. + * + * @format uri + */ + uri: string; + }; + } + + /** + * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. + */ + export interface ResourceUpdatedNotification extends Notification { + method: "notifications/resources/updated"; + params: { + /** + * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. + * + * @format uri + */ + uri: string; + }; + } + + /** + * A known resource that the server is capable of reading. + */ + export interface Resource extends Annotated { + /** + * The URI of this resource. + * + * @format uri + */ + uri: string; + + /** + * A human-readable name for this resource. + * + * This can be used by clients to populate UI elements. + */ + name: string; + + /** + * A description of what this resource represents. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * The MIME type of this resource, if known. + */ + mimeType?: string; + + /** + * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. + * + * This can be used by Hosts to display file sizes and estimate context window usage. + */ + size?: number; + } + + /** + * A template description for resources available on the server. + */ + export interface ResourceTemplate extends Annotated { + /** + * A URI template (according to RFC 6570) that can be used to construct resource URIs. + * + * @format uri-template + */ + uriTemplate: string; + + /** + * A human-readable name for the type of resource this template refers to. + * + * This can be used by clients to populate UI elements. + */ + name: string; + + /** + * A description of what this template is for. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. + */ + mimeType?: string; + } + + /** + * The contents of a specific resource or sub-resource. + */ + export interface ResourceContents { + /** + * The URI of this resource. + * + * @format uri + */ + uri: string; + /** + * The MIME type of this resource, if known. + */ + mimeType?: string; + } + + export interface TextResourceContents extends ResourceContents { + /** + * The text of the item. This must only be set if the item can actually be represented as text (not binary data). + */ + text: string; + } + + export interface BlobResourceContents extends ResourceContents { + /** + * A base64-encoded string representing the binary data of the item. + * + * @format byte + */ + blob: string; + } + + /* Prompts */ + /** + * Sent from the client to request a list of prompts and prompt templates the server has. + */ + export interface ListPromptsRequest extends PaginatedRequest { + method: "prompts/list"; + } + + /** + * The server's response to a prompts/list request from the client. + */ + export interface ListPromptsResult extends PaginatedResult { + prompts: Prompt[]; + } + + /** + * Used by the client to get a prompt provided by the server. + */ + export interface GetPromptRequest extends Request { + method: "prompts/get"; + params: { + /** + * The name of the prompt or prompt template. + */ + name: string; + /** + * Arguments to use for templating the prompt. + */ + arguments?: { [key: string]: string }; + }; + } + + /** + * The server's response to a prompts/get request from the client. + */ + export interface GetPromptResult extends Result { + /** + * An optional description for the prompt. + */ + description?: string; + messages: PromptMessage[]; + } + + /** + * A prompt or prompt template that the server offers. + */ + export interface Prompt { + /** + * The name of the prompt or prompt template. + */ + name: string; + /** + * An optional description of what this prompt provides + */ + description?: string; + /** + * A list of arguments to use for templating the prompt. + */ + arguments?: PromptArgument[]; + } + + /** + * Describes an argument that a prompt can accept. + */ + export interface PromptArgument { + /** + * The name of the argument. + */ + name: string; + /** + * A human-readable description of the argument. + */ + description?: string; + /** + * Whether this argument must be provided. + */ + required?: boolean; + } + + /** + * The sender or recipient of messages and data in a conversation. + */ + export type Role = "user" | "assistant"; + + /** + * Describes a message returned as part of a prompt. + * + * This is similar to `SamplingMessage`, but also supports the embedding of + * resources from the MCP server. + */ + export interface PromptMessage { + role: Role; + content: TextContent | ImageContent | EmbeddedResource; + } + + /** + * The contents of a resource, embedded into a prompt or tool call result. + * + * It is up to the client how best to render embedded resources for the benefit + * of the LLM and/or the user. + */ + export interface EmbeddedResource extends Annotated { + type: "resource"; + resource: TextResourceContents | BlobResourceContents; + } + + /** + * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. + */ + export interface PromptListChangedNotification extends Notification { + method: "notifications/prompts/list_changed"; + } + + /* Tools */ + /** + * Sent from the client to request a list of tools the server has. + */ + export interface ListToolsRequest extends PaginatedRequest { + method: "tools/list"; + } + + /** + * The server's response to a tools/list request from the client. + */ + export interface ListToolsResult extends PaginatedResult { + tools: Tool[]; + } + + /** + * The server's response to a tool call. + * + * Any errors that originate from the tool SHOULD be reported inside the result + * object, with `isError` set to true, _not_ as an MCP protocol-level error + * response. Otherwise, the LLM would not be able to see that an error occurred + * and self-correct. + * + * However, any errors in _finding_ the tool, an error indicating that the + * server does not support tool calls, or any other exceptional conditions, + * should be reported as an MCP error response. + */ + export interface CallToolResult extends Result { + content: (TextContent | ImageContent | EmbeddedResource)[]; + + /** + * Whether the tool call ended in an error. + * + * If not set, this is assumed to be false (the call was successful). + */ + isError?: boolean; + } + + /** + * Used by the client to invoke a tool provided by the server. + */ + export interface CallToolRequest extends Request { + method: "tools/call"; + params: { + name: string; + arguments?: { [key: string]: unknown }; + }; + } + + /** + * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. + */ + export interface ToolListChangedNotification extends Notification { + method: "notifications/tools/list_changed"; + } + + /** + * Definition for a tool the client can call. + */ + export interface Tool { + /** + * The name of the tool. + */ + name: string; + /** + * A human-readable description of the tool. + */ + description?: string; + /** + * A JSON Schema object defining the expected parameters for the tool. + */ + inputSchema: { + type: "object"; + properties?: { [key: string]: object }; + required?: string[]; + }; + } + + /* Logging */ + /** + * A request from the client to the server, to enable or adjust logging. + */ + export interface SetLevelRequest extends Request { + method: "logging/setLevel"; + params: { + /** + * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/message. + */ + level: LoggingLevel; + }; + } + + /** + * Notification of a log message passed from server to client. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. + */ + export interface LoggingMessageNotification extends Notification { + method: "notifications/message"; + params: { + /** + * The severity of this log message. + */ + level: LoggingLevel; + /** + * An optional name of the logger issuing this message. + */ + logger?: string; + /** + * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + */ + data: unknown; + }; + } + + /** + * The severity of a log message. + * + * These map to syslog message severities, as specified in RFC-5424: + * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 + */ + export type LoggingLevel = + | "debug" + | "info" + | "notice" + | "warning" + | "error" + | "critical" + | "alert" + | "emergency"; + + /* Sampling */ + /** + * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. + */ + export interface CreateMessageRequest extends Request { + method: "sampling/createMessage"; + params: { + messages: SamplingMessage[]; + /** + * The server's preferences for which model to select. The client MAY ignore these preferences. + */ + modelPreferences?: ModelPreferences; + /** + * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. + */ + systemPrompt?: string; + /** + * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. The client MAY ignore this request. + */ + includeContext?: "none" | "thisServer" | "allServers"; + /** + * @TJS-type number + */ + temperature?: number; + /** + * The maximum number of tokens to sample, as requested by the server. The client MAY choose to sample fewer tokens than requested. + */ + maxTokens: number; + stopSequences?: string[]; + /** + * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. + */ + metadata?: object; + }; + } + + /** + * The client's response to a sampling/create_message request from the server. The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) and decide whether to allow the server to see it. + */ + export interface CreateMessageResult extends Result, SamplingMessage { + /** + * The name of the model that generated the message. + */ + model: string; + /** + * The reason why sampling stopped, if known. + */ + stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string; + } + + /** + * Describes a message issued to or received from an LLM API. + */ + export interface SamplingMessage { + role: Role; + content: TextContent | ImageContent; + } + + /** + * Base for objects that include optional annotations for the client. The client can use annotations to inform how objects are used or displayed + */ + export interface Annotated { + annotations?: { + /** + * Describes who the intended customer of this object or data is. + * + * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). + */ + audience?: Role[]; + + /** + * Describes how important this data is for operating the server. + * + * A value of 1 means "most important," and indicates that the data is + * effectively required, while 0 means "least important," and indicates that + * the data is entirely optional. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + priority?: number; + } + } + + /** + * Text provided to or from an LLM. + */ + export interface TextContent extends Annotated { + type: "text"; + /** + * The text content of the message. + */ + text: string; + } + + /** + * An image provided to or from an LLM. + */ + export interface ImageContent extends Annotated { + type: "image"; + /** + * The base64-encoded image data. + * + * @format byte + */ + data: string; + /** + * The MIME type of the image. Different providers may support different image types. + */ + mimeType: string; + } + + /** + * The server's preferences for model selection, requested of the client during sampling. + * + * Because LLMs can vary along multiple dimensions, choosing the "best" model is + * rarely straightforward. Different models excel in different areas-some are + * faster but less capable, others are more capable but more expensive, and so + * on. This interface allows servers to express their priorities across multiple + * dimensions to help clients make an appropriate selection for their use case. + * + * These preferences are always advisory. The client MAY ignore them. It is also + * up to the client to decide how to interpret these preferences and how to + * balance them against other considerations. + */ + export interface ModelPreferences { + /** + * Optional hints to use for model selection. + * + * If multiple hints are specified, the client MUST evaluate them in order + * (such that the first match is taken). + * + * The client SHOULD prioritize these hints over the numeric priorities, but + * MAY still use the priorities to select from ambiguous matches. + */ + hints?: ModelHint[]; + + /** + * How much to prioritize cost when selecting a model. A value of 0 means cost + * is not important, while a value of 1 means cost is the most important + * factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + costPriority?: number; + + /** + * How much to prioritize sampling speed (latency) when selecting a model. A + * value of 0 means speed is not important, while a value of 1 means speed is + * the most important factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + speedPriority?: number; + + /** + * How much to prioritize intelligence and capabilities when selecting a + * model. A value of 0 means intelligence is not important, while a value of 1 + * means intelligence is the most important factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + intelligencePriority?: number; + } + + /** + * Hints to use for model selection. + * + * Keys not declared here are currently left unspecified by the spec and are up + * to the client to interpret. + */ + export interface ModelHint { + /** + * A hint for a model name. + * + * The client SHOULD treat this as a substring of a model name; for example: + * - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022` + * - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc. + * - `claude` should match any Claude model + * + * The client MAY also map the string to a different provider's model name or a different model family, as long as it fills a similar niche; for example: + * - `gemini-1.5-flash` could match `claude-3-haiku-20240307` + */ + name?: string; + } + + /* Autocomplete */ + /** + * A request from the client to the server, to ask for completion options. + */ + export interface CompleteRequest extends Request { + method: "completion/complete"; + params: { + ref: PromptReference | ResourceReference; + /** + * The argument's information + */ + argument: { + /** + * The name of the argument + */ + name: string; + /** + * The value of the argument to use for completion matching. + */ + value: string; + }; + }; + } + + /** + * The server's response to a completion/complete request + */ + export interface CompleteResult extends Result { + completion: { + /** + * An array of completion values. Must not exceed 100 items. + */ + values: string[]; + /** + * The total number of completion options available. This can exceed the number of values actually sent in the response. + */ + total?: number; + /** + * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. + */ + hasMore?: boolean; + }; + } + + /** + * A reference to a resource or resource template definition. + */ + export interface ResourceReference { + type: "ref/resource"; + /** + * The URI or URI template of the resource. + * + * @format uri-template + */ + uri: string; + } + + /** + * Identifies a prompt. + */ + export interface PromptReference { + type: "ref/prompt"; + /** + * The name of the prompt or prompt template + */ + name: string; + } + + /* Roots */ + /** + * Sent from the server to request a list of root URIs from the client. Roots allow + * servers to ask for specific directories or files to operate on. A common example + * for roots is providing a set of repositories or directories a server should operate + * on. + * + * This request is typically used when the server needs to understand the file system + * structure or access specific locations that the client has permission to read from. + */ + export interface ListRootsRequest extends Request { + method: "roots/list"; + } + + /** + * The client's response to a roots/list request from the server. + * This result contains an array of Root objects, each representing a root directory + * or file that the server can operate on. + */ + export interface ListRootsResult extends Result { + roots: Root[]; + } + + /** + * Represents a root directory or file that the server can operate on. + */ + export interface Root { + /** + * The URI identifying the root. This *must* start with file:// for now. + * This restriction may be relaxed in future versions of the protocol to allow + * other URI schemes. + * + * @format uri + */ + uri: string; + /** + * An optional name for the root. This can be used to provide a human-readable + * identifier for the root, which may be useful for display purposes or for + * referencing the root in other parts of the application. + */ + name?: string; + } + + /** + * A notification from the client to the server, informing it that the list of roots has changed. + * This notification should be sent whenever the client adds, removes, or modifies any root. + * The server should then request an updated list of roots using the ListRootsRequest. + */ + export interface RootsListChangedNotification extends Notification { + method: "notifications/roots/list_changed"; + } + + /* Client messages */ + export type ClientRequest = + | PingRequest + | InitializeRequest + | CompleteRequest + | SetLevelRequest + | GetPromptRequest + | ListPromptsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscribeRequest + | UnsubscribeRequest + | CallToolRequest + | ListToolsRequest; + + export type ClientNotification = + | CancelledNotification + | ProgressNotification + | InitializedNotification + | RootsListChangedNotification; + + export type ClientResult = EmptyResult | CreateMessageResult | ListRootsResult; + + /* Server messages */ + export type ServerRequest = + | PingRequest + | CreateMessageRequest + | ListRootsRequest; + + export type ServerNotification = + | CancelledNotification + | ProgressNotification + | LoggingMessageNotification + | ResourceUpdatedNotification + | ResourceListChangedNotification + | ToolListChangedNotification + | PromptListChangedNotification; + + export type ServerResult = + | EmptyResult + | InitializeResult + | CompleteResult + | GetPromptResult + | ListPromptsResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | CallToolResult + | ListToolsResult; +} diff --git a/src/vs/workbench/contrib/mcp/electron-sandbox/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/electron-sandbox/mcp.contribution.ts new file mode 100644 index 00000000000..efdf3ee192d --- /dev/null +++ b/src/vs/workbench/contrib/mcp/electron-sandbox/mcp.contribution.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js'; +import { NativeMcpDiscovery } from './nativeMpcDiscovery.js'; + +mcpDiscoveryRegistry.register(new SyncDescriptor(NativeMcpDiscovery)); diff --git a/src/vs/workbench/contrib/mcp/electron-sandbox/nativeMpcDiscovery.ts b/src/vs/workbench/contrib/mcp/electron-sandbox/nativeMpcDiscovery.ts new file mode 100644 index 00000000000..11da0b14991 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/electron-sandbox/nativeMpcDiscovery.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { INativeMcpDiscoveryHelperService, NativeMcpDiscoveryHelperChannelName } from '../../../../platform/mcp/common/nativeMcpDiscoveryHelper.js'; +import { FilesystemMpcDiscovery } from '../common/discovery/nativeMcpDiscoveryAbstract.js'; +import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; + +export class NativeMcpDiscovery extends FilesystemMpcDiscovery { + constructor( + @IMainProcessService private readonly mainProcess: IMainProcessService, + @ILogService private readonly logService: ILogService, + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService, + @IInstantiationService instantiationService: IInstantiationService, + @IMcpRegistry mcpRegistry: IMcpRegistry, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(null, labelService, fileService, instantiationService, mcpRegistry, configurationService); + } + + public override start(): void { + const service = ProxyChannel.toService( + this.mainProcess.getChannel(NativeMcpDiscoveryHelperChannelName)); + + service.load().then( + data => this.setDetails(data), + err => { + this.logService.warn('Error getting main process MCP environment', err); + this.setDetails(undefined); + } + ); + } +} diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts new file mode 100644 index 00000000000..774878b19d2 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts @@ -0,0 +1,591 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as sinon from 'sinon'; +import { cloneAndChange } from '../../../../../base/common/objects.js'; +import { ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { upcast } from '../../../../../base/common/types.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js'; +import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILoggerService } from '../../../../../platform/log/common/log.js'; +import { ISecretStorageService } from '../../../../../platform/secrets/common/secrets.js'; +import { TestSecretStorageService } from '../../../../../platform/secrets/test/common/testSecretStorageService.js'; +import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js'; +import { IConfigurationResolverService } from '../../../../services/configurationResolver/common/configurationResolver.js'; +import { IOutputService } from '../../../../services/output/common/output.js'; +import { TestLoggerService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { McpRegistry } from '../../common/mcpRegistry.js'; +import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js'; +import { McpServerConnection } from '../../common/mcpServerConnection.js'; +import { LazyCollectionState, McpCollectionDefinition, McpCollectionReference, McpServerDefinition, McpServerTransportType } from '../../common/mcpTypes.js'; +import { TestMcpMessageTransport } from './mcpRegistryTypes.js'; +import { timeout } from '../../../../../base/common/async.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; + +class TestConfigurationResolverService implements Partial { + declare readonly _serviceBrand: undefined; + + private interactiveCounter = 0; + + // Used to simulate stored/resolved variables + private readonly resolvedVariables = new Map(); + + constructor() { + // Add some test variables + this.resolvedVariables.set('workspaceFolder', '/test/workspace'); + this.resolvedVariables.set('fileBasename', 'test.txt'); + } + + resolveAsync(folder: any, value: any): Promise { + if (typeof value === 'string') { + return Promise.resolve(this.replaceVariables(value)); + } else if (Array.isArray(value)) { + return Promise.resolve(value.map(v => typeof v === 'string' ? this.replaceVariables(v) : v)); + } else { + const result: Record = {}; + for (const key in value) { + if (typeof value[key] === 'string') { + result[key] = this.replaceVariables(value[key]); + } else { + result[key] = value[key]; + } + } + return Promise.resolve(result); + } + } + + private replaceVariables(value: string): string { + let result = value; + for (const [key, val] of this.resolvedVariables.entries()) { + result = result.replace(`\${${key}}`, val); + } + return result; + } + + resolveAnyAsync(folder: any, config: any, commandValueMapping?: Record): Promise { + // Use cloneAndChange to recursively replace variables in the config + const newConfig = cloneAndChange(config, (value) => { + if (typeof value === 'string') { + // Replace any ${variable} with its value + let result = value; + for (const [key, val] of this.resolvedVariables.entries()) { + result = result.replace(`\${${key}}`, val); + } + + // If a commandValueMapping is provided, use it for additional replacements + if (commandValueMapping) { + for (const [key, val] of Object.entries(commandValueMapping)) { + result = result.replace(`\${${key}}`, val); + } + } + + return result === value ? undefined : result; + } + return undefined; + }); + + return Promise.resolve(newConfig); + } + + resolveWithInteraction(folder: any, config: any, section?: string, variables?: Record, target?: ConfigurationTarget): Promise | undefined> { + // For testing, we simulate interaction by returning a map with some variables + const result = new Map(); + result.set('input:testInteractive', `interactiveValue${this.interactiveCounter++}`); + result.set('command:testCommand', `commandOutput${this.interactiveCounter++}}`); + + // If variables are provided, include those too + if (variables) { + Object.entries(variables).forEach(([key, value]) => { + result.set(key, value); + }); + } + + return Promise.resolve(result); + } +} + +class TestMcpHostDelegate implements IMcpHostDelegate { + canStart(): boolean { + return true; + } + + start(): IMcpMessageTransport { + return new TestMcpMessageTransport(); + } + + waitForInitialProviderPromises(): Promise { + return Promise.resolve(); + } +} + +class TestDialogService implements Partial { + declare readonly _serviceBrand: undefined; + + private _promptResult: boolean | undefined; + private _promptSpy: sinon.SinonStub; + + constructor() { + this._promptSpy = sinon.stub(); + this._promptSpy.callsFake(() => { + return Promise.resolve({ result: this._promptResult }); + }); + } + + setPromptResult(result: boolean | undefined): void { + this._promptResult = result; + } + + get promptSpy(): sinon.SinonStub { + return this._promptSpy; + } + + prompt(options: any): Promise { + return this._promptSpy(options); + } +} + +suite('Workbench - MCP - Registry', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let registry: McpRegistry; + let testStorageService: TestStorageService; + let testConfigResolverService: TestConfigurationResolverService; + let testDialogService: TestDialogService; + let testCollection: McpCollectionDefinition & { serverDefinitions: ISettableObservable }; + let baseDefinition: McpServerDefinition; + + setup(() => { + testConfigResolverService = new TestConfigurationResolverService(); + testStorageService = store.add(new TestStorageService()); + testDialogService = new TestDialogService(); + + const services = new ServiceCollection( + [IConfigurationResolverService, testConfigResolverService], + [IStorageService, testStorageService], + [ISecretStorageService, new TestSecretStorageService()], + [ILoggerService, store.add(new TestLoggerService())], + [IOutputService, upcast({ showChannel: () => { } })], + [IDialogService, testDialogService], + [IProductService, {}], + ); + + const instaService = store.add(new TestInstantiationService(services)); + registry = store.add(instaService.createInstance(McpRegistry)); + + // Create test collection that can be reused + testCollection = { + id: 'test-collection', + label: 'Test Collection', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + // Create base definition that can be reused + baseDefinition = { + id: 'test-server', + label: 'Test Server', + launch: { + type: McpServerTransportType.Stdio, + command: 'test-command', + args: [], + env: {}, + cwd: URI.parse('file:///test') + } + }; + }); + + test('registerCollection adds collection to registry', () => { + const disposable = registry.registerCollection(testCollection); + store.add(disposable); + + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.collections.get()[0], testCollection); + + disposable.dispose(); + assert.strictEqual(registry.collections.get().length, 0); + }); + + test('registerDelegate adds delegate to registry', () => { + const delegate = new TestMcpHostDelegate(); + const disposable = registry.registerDelegate(delegate); + store.add(disposable); + + assert.strictEqual(registry.delegates.length, 1); + assert.strictEqual(registry.delegates[0], delegate); + + disposable.dispose(); + assert.strictEqual(registry.delegates.length, 0); + }); + + test('resolveConnection creates connection with resolved variables and memorizes them until cleared', async () => { + const definition: McpServerDefinition = { + ...baseDefinition, + launch: { + type: McpServerTransportType.Stdio, + command: '${workspaceFolder}/cmd', + args: ['--file', '${fileBasename}'], + env: { + PATH: '${input:testInteractive}' + }, + cwd: URI.parse('file:///test') + }, + variableReplacement: { + section: 'mcp' + } + }; + + const delegate = new TestMcpHostDelegate(); + store.add(registry.registerDelegate(delegate)); + testCollection.serverDefinitions.set([definition], undefined); + store.add(registry.registerCollection(testCollection)); + + const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition }) as McpServerConnection; + + assert.ok(connection); + assert.strictEqual(connection.definition, definition); + assert.strictEqual((connection.launchDefinition as any).command, '/test/workspace/cmd'); + assert.strictEqual((connection.launchDefinition as any).env.PATH, 'interactiveValue0'); + connection.dispose(); + + const connection2 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition }) as McpServerConnection; + + assert.ok(connection2); + assert.strictEqual((connection2.launchDefinition as any).env.PATH, 'interactiveValue0'); + connection2.dispose(); + + registry.clearSavedInputs(); + + const connection3 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition }) as McpServerConnection; + + assert.ok(connection3); + assert.strictEqual((connection3.launchDefinition as any).env.PATH, 'interactiveValue4'); + connection3.dispose(); + }); + + suite('Trust Management', () => { + setup(() => { + const delegate = new TestMcpHostDelegate(); + store.add(registry.registerDelegate(delegate)); + }); + + test('resolveConnection connects to server when trusted by default', async () => { + const definition = { ...baseDefinition }; + store.add(registry.registerCollection(testCollection)); + testCollection.serverDefinitions.set([definition], undefined); + + const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition }); + + assert.ok(connection); + assert.strictEqual(testDialogService.promptSpy.called, false); + connection?.dispose(); + }); + + test('resolveConnection prompts for confirmation when not trusted by default', async () => { + const untrustedCollection: McpCollectionDefinition = { + ...testCollection, + isTrustedByDefault: false + }; + + const definition = { ...baseDefinition }; + store.add(registry.registerCollection(untrustedCollection)); + testCollection.serverDefinitions.set([definition], undefined); + + testDialogService.setPromptResult(true); + + const connection = await registry.resolveConnection({ + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.ok(connection); + assert.strictEqual(testDialogService.promptSpy.called, true); + connection?.dispose(); + + testDialogService.promptSpy.resetHistory(); + const connection2 = await registry.resolveConnection({ + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.ok(connection2); + assert.strictEqual(testDialogService.promptSpy.called, false); + connection2?.dispose(); + }); + + test('resolveConnection returns undefined when user does not trust the server', async () => { + const untrustedCollection: McpCollectionDefinition = { + ...testCollection, + isTrustedByDefault: false + }; + + const definition = { ...baseDefinition }; + store.add(registry.registerCollection(untrustedCollection)); + testCollection.serverDefinitions.set([definition], undefined); + + testDialogService.setPromptResult(false); + + const connection = await registry.resolveConnection({ + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.strictEqual(connection, undefined); + assert.strictEqual(testDialogService.promptSpy.called, true); + + testDialogService.promptSpy.resetHistory(); + const connection2 = await registry.resolveConnection({ + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.strictEqual(connection2, undefined); + assert.strictEqual(testDialogService.promptSpy.called, false); + }); + + test('resolveConnection honors forceTrust parameter', async () => { + const untrustedCollection: McpCollectionDefinition = { + ...testCollection, + isTrustedByDefault: false + }; + + const definition = { ...baseDefinition }; + store.add(registry.registerCollection(untrustedCollection)); + testCollection.serverDefinitions.set([definition], undefined); + + testDialogService.setPromptResult(false); + + const connection1 = await registry.resolveConnection({ + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.strictEqual(connection1, undefined); + + testDialogService.promptSpy.resetHistory(); + testDialogService.setPromptResult(true); + + const connection2 = await registry.resolveConnection({ + collectionRef: untrustedCollection, + definitionRef: definition, + forceTrust: true + }); + + assert.ok(connection2); + assert.strictEqual(testDialogService.promptSpy.called, true); + connection2?.dispose(); + + testDialogService.promptSpy.resetHistory(); + const connection3 = await registry.resolveConnection({ + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.ok(connection3); + assert.strictEqual(testDialogService.promptSpy.called, false); + connection3?.dispose(); + }); + }); + + suite('Lazy Collections', () => { + let lazyCollection: McpCollectionDefinition; + let normalCollection: McpCollectionDefinition; + let removedCalled: boolean; + + setup(() => { + removedCalled = false; + lazyCollection = { + ...testCollection, + id: 'lazy-collection', + lazy: { + isCached: false, + load: () => Promise.resolve(), + removed: () => { removedCalled = true; } + } + }; + normalCollection = { + ...testCollection, + id: 'lazy-collection', + serverDefinitions: observableValue('serverDefs', [baseDefinition]) + }; + }); + + test('registers lazy collection', () => { + const disposable = registry.registerCollection(lazyCollection); + store.add(disposable); + + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.collections.get()[0], lazyCollection); + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.HasUnknown); + }); + + test('lazy collection is replaced by normal collection', () => { + store.add(registry.registerCollection(lazyCollection)); + store.add(registry.registerCollection(normalCollection)); + + const collections = registry.collections.get(); + assert.strictEqual(collections.length, 1); + assert.strictEqual(collections[0], normalCollection); + assert.strictEqual(collections[0].lazy, undefined); + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.AllKnown); + }); + + test('lazyCollectionState updates correctly during loading', async () => { + lazyCollection = { + ...lazyCollection, + lazy: { + ...lazyCollection.lazy!, + load: async () => { + await timeout(0); + store.add(registry.registerCollection(normalCollection)); + return Promise.resolve(); + } + } + }; + + store.add(registry.registerCollection(lazyCollection)); + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.HasUnknown); + + const loadingPromise = registry.discoverCollections(); + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.LoadingUnknown); + + await loadingPromise; + + // The collection wasn't replaced, so it should be removed + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.AllKnown); + assert.strictEqual(removedCalled, false); + }); + + test('removed callback is called when lazy collection is not replaced', async () => { + store.add(registry.registerCollection(lazyCollection)); + await registry.discoverCollections(); + + assert.strictEqual(removedCalled, true); + }); + + test('cached lazy collections are tracked correctly', () => { + lazyCollection.lazy!.isCached = true; + store.add(registry.registerCollection(lazyCollection)); + + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.AllKnown); + + // Adding an uncached lazy collection changes the state + const uncachedLazy = { + ...lazyCollection, + id: 'uncached-lazy', + lazy: { + ...lazyCollection.lazy!, + isCached: false + } + }; + store.add(registry.registerCollection(uncachedLazy)); + + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.HasUnknown); + }); + }); + + suite('Collection Tool Prefixes', () => { + test('assigns unique prefixes to collections', () => { + const collection1: McpCollectionDefinition = { + id: 'collection1', + label: 'Collection 1', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + const collection2: McpCollectionDefinition = { + id: 'collection2', + label: 'Collection 2', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + store.add(registry.registerCollection(collection1)); + store.add(registry.registerCollection(collection2)); + + const prefix1 = registry.collectionToolPrefix(collection1).get(); + const prefix2 = registry.collectionToolPrefix(collection2).get(); + + assert.notStrictEqual(prefix1, prefix2); + assert.ok(/^[a-f0-9]{3}\.$/.test(prefix1)); + assert.ok(/^[a-f0-9]{3}\.$/.test(prefix2)); + }); + + test('handles hash collisions by incrementing view', () => { + // These strings are known to have SHA1 hash collisions in their first 3 characters + const collection1: McpCollectionDefinition = { + id: 'potato', + label: 'Collection 1', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + const collection2: McpCollectionDefinition = { + id: 'candidate_83048', + label: 'Collection 2', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + store.add(registry.registerCollection(collection1)); + store.add(registry.registerCollection(collection2)); + + const prefix1 = registry.collectionToolPrefix(collection1).get(); + const prefix2 = registry.collectionToolPrefix(collection2).get(); + + assert.notStrictEqual(prefix1, prefix2); + assert.ok(/^[a-f0-9]{3}\.$/.test(prefix1)); + assert.ok(/^[a-f0-9]{3}\.$/.test(prefix2)); + }); + + test('prefix changes when collections change', () => { + const collection1: McpCollectionDefinition = { + id: 'collection1', + label: 'Collection 1', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + const disposable = registry.registerCollection(collection1); + store.add(disposable); + + const prefix1 = registry.collectionToolPrefix(collection1).get(); + assert.ok(!!prefix1); + + disposable.dispose(); + + const prefix2 = registry.collectionToolPrefix(collection1).get(); + + assert.strictEqual(prefix2, ''); + }); + + test('prefix is empty for unknown collections', () => { + const unknownCollection: McpCollectionReference = { + id: 'unknown', + label: 'Unknown' + }; + + const prefix = registry.collectionToolPrefix(unknownCollection).get(); + assert.strictEqual(prefix, ''); + }); + }); +}); diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistryInputStorage.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryInputStorage.test.ts new file mode 100644 index 00000000000..f6f745d5751 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryInputStorage.test.ts @@ -0,0 +1,178 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; +import { TestSecretStorageService } from '../../../../../platform/secrets/test/common/testSecretStorageService.js'; +import { StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { McpRegistryInputStorage } from '../../common/mcpRegistryInputStorage.js'; + +suite('Workbench - MCP - RegistryInputStorage', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let testStorageService: TestStorageService; + let testSecretStorageService: TestSecretStorageService; + let testLogService: ILogService; + let mcpInputStorage: McpRegistryInputStorage; + + setup(() => { + testStorageService = store.add(new TestStorageService()); + testSecretStorageService = new TestSecretStorageService(); + testLogService = store.add(new NullLogService()); + + // Create the input storage with APPLICATION scope + mcpInputStorage = store.add(new McpRegistryInputStorage( + StorageScope.APPLICATION, + StorageTarget.MACHINE, + testStorageService, + testSecretStorageService, + testLogService + )); + }); + + test('setPlainText stores values that can be retrieved with getMap', async () => { + const values = { + 'key1': 'value1', + 'key2': 'value2' + }; + + await mcpInputStorage.setPlainText(values); + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.key1, 'value1'); + assert.strictEqual(result.key2, 'value2'); + }); + + test('setSecrets stores encrypted values that can be retrieved with getMap', async () => { + const secrets = { + 'secretKey1': 'secretValue1', + 'secretKey2': 'secretValue2' + }; + + await mcpInputStorage.setSecrets(secrets); + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.secretKey1, 'secretValue1'); + assert.strictEqual(result.secretKey2, 'secretValue2'); + }); + + test('getMap returns combined plain text and secret values', async () => { + await mcpInputStorage.setPlainText({ + 'plainKey': 'plainValue' + }); + + await mcpInputStorage.setSecrets({ + 'secretKey': 'secretValue' + }); + + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.plainKey, 'plainValue'); + assert.strictEqual(result.secretKey, 'secretValue'); + }); + + test('clear removes specific values', async () => { + await mcpInputStorage.setPlainText({ + 'key1': 'value1', + 'key2': 'value2' + }); + + await mcpInputStorage.setSecrets({ + 'secretKey1': 'secretValue1', + 'secretKey2': 'secretValue2' + }); + + // Clear one plain and one secret value + await mcpInputStorage.clear('key1'); + await mcpInputStorage.clear('secretKey1'); + + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.key1, undefined); + assert.strictEqual(result.key2, 'value2'); + assert.strictEqual(result.secretKey1, undefined); + assert.strictEqual(result.secretKey2, 'secretValue2'); + }); + + test('clearAll removes all values', async () => { + await mcpInputStorage.setPlainText({ + 'key1': 'value1' + }); + + await mcpInputStorage.setSecrets({ + 'secretKey1': 'secretValue1' + }); + + mcpInputStorage.clearAll(); + + const result = await mcpInputStorage.getMap(); + + assert.deepStrictEqual(result, {}); + }); + + test('updates to plain text values overwrite existing values', async () => { + await mcpInputStorage.setPlainText({ + 'key1': 'value1', + 'key2': 'value2' + }); + + await mcpInputStorage.setPlainText({ + 'key1': 'updatedValue1' + }); + + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.key1, 'updatedValue1'); + assert.strictEqual(result.key2, 'value2'); + }); + + test('updates to secret values overwrite existing values', async () => { + await mcpInputStorage.setSecrets({ + 'secretKey1': 'secretValue1', + 'secretKey2': 'secretValue2' + }); + + await mcpInputStorage.setSecrets({ + 'secretKey1': 'updatedSecretValue1' + }); + + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.secretKey1, 'updatedSecretValue1'); + assert.strictEqual(result.secretKey2, 'secretValue2'); + }); + + test('storage persists values across instances', async () => { + // Set values on first instance + await mcpInputStorage.setPlainText({ + 'key1': 'value1' + }); + + await mcpInputStorage.setSecrets({ + 'secretKey1': 'secretValue1' + }); + + await testStorageService.flush(); + + // Create a second instance that should have access to the same storage + const secondInstance = store.add(new McpRegistryInputStorage( + StorageScope.APPLICATION, + StorageTarget.MACHINE, + testStorageService, + testSecretStorageService, + testLogService + )); + + const result = await secondInstance.getMap(); + + assert.strictEqual(result.key1, 'value1'); + assert.strictEqual(result.secretKey1, 'secretValue1'); + + assert.ok(!testStorageService.get('mcpInputs', StorageScope.APPLICATION)?.includes('secretValue1')); + }); +}); + diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts new file mode 100644 index 00000000000..567c613329d --- /dev/null +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from '../../../../../base/common/event.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../base/common/observable.js'; +import { IMcpMessageTransport } from '../../common/mcpRegistryTypes.js'; +import { McpConnectionState } from '../../common/mcpTypes.js'; +import { MCP } from '../../common/modelContextProtocol.js'; + +/** + * Implementation of IMcpMessageTransport for testing purposes. + * Allows tests to easily send/receive messages and control the connection state. + */ +export class TestMcpMessageTransport extends Disposable implements IMcpMessageTransport { + private readonly _onDidLog = this._register(new Emitter()); + public readonly onDidLog = this._onDidLog.event; + + private readonly _onDidReceiveMessage = this._register(new Emitter()); + public readonly onDidReceiveMessage = this._onDidReceiveMessage.event; + + private readonly _stateValue = observableValue('testTransportState', { state: McpConnectionState.Kind.Starting }); + public readonly state = this._stateValue; + + private readonly _sentMessages: MCP.JSONRPCMessage[] = []; + + constructor() { + super(); + } + + /** + * Send a message through the transport. + */ + public send(message: MCP.JSONRPCMessage): void { + this._sentMessages.push(message); + } + + /** + * Stop the transport. + */ + public stop(): void { + this._stateValue.set({ state: McpConnectionState.Kind.Stopped }, undefined); + } + + // Test Helper Methods + + /** + * Simulate receiving a message from the server. + */ + public simulateReceiveMessage(message: MCP.JSONRPCMessage): void { + this._onDidReceiveMessage.fire(message); + } + + /** + * Simulates a reply to an 'initialized' request. + */ + public simulateInitialized() { + if (!this._sentMessages.length) { + throw new Error('initialize was not called yet'); + } + + this.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: (this.getSentMessages()[0] as MCP.JSONRPCRequest).id, + result: { + protocolVersion: MCP.LATEST_PROTOCOL_VERSION, + capabilities: { + tools: {}, + }, + serverInfo: { + name: 'Test Server', + version: '1.0.0' + }, + } satisfies MCP.InitializeResult + }); + } + + /** + * Simulate a log event. + */ + public simulateLog(message: string): void { + this._onDidLog.fire(message); + } + + /** + * Set the connection state. + */ + public setConnectionState(state: McpConnectionState): void { + this._stateValue.set(state, undefined); + } + + /** + * Get all messages that have been sent. + */ + public getSentMessages(): readonly MCP.JSONRPCMessage[] { + return [...this._sentMessages]; + } + + /** + * Clear the sent messages history. + */ + public clearSentMessages(): void { + this._sentMessages.length = 0; + } +} diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts new file mode 100644 index 00000000000..83b9933c3f2 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts @@ -0,0 +1,429 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { timeout } from '../../../../../base/common/async.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, observableValue } from '../../../../../base/common/observable.js'; +import { upcast } from '../../../../../base/common/types.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILogger, ILoggerService } from '../../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js'; +import { IOutputService } from '../../../../services/output/common/output.js'; +import { TestLoggerService, TestProductService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js'; +import { McpServerConnection } from '../../common/mcpServerConnection.js'; +import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerTransportType } from '../../common/mcpTypes.js'; +import { TestMcpMessageTransport } from './mcpRegistryTypes.js'; + +class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate { + private readonly _transport: TestMcpMessageTransport; + private _canStartValue = true; + + constructor() { + super(); + this._transport = this._register(new TestMcpMessageTransport()); + } + + canStart(): boolean { + return this._canStartValue; + } + + start(): IMcpMessageTransport { + if (!this._canStartValue) { + throw new Error('Cannot start server'); + } + return this._transport; + } + + getTransport(): TestMcpMessageTransport { + return this._transport; + } + + setCanStart(value: boolean): void { + this._canStartValue = value; + } + + waitForInitialProviderPromises(): Promise { + return Promise.resolve(); + } +} + +suite('Workbench - MCP - ServerConnection', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + let delegate: TestMcpHostDelegate; + let transport: TestMcpMessageTransport; + let collection: McpCollectionDefinition; + let serverDefinition: McpServerDefinition; + + setup(() => { + delegate = store.add(new TestMcpHostDelegate()); + transport = delegate.getTransport(); + + // Setup test services + const services = new ServiceCollection( + [ILoggerService, store.add(new TestLoggerService())], + [IOutputService, upcast({ showChannel: () => { } })], + [IStorageService, store.add(new TestStorageService())], + [IProductService, TestProductService], + ); + + instantiationService = store.add(new TestInstantiationService(services)); + + // Create test collection + collection = { + id: 'test-collection', + label: 'Test Collection', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + // Create server definition + serverDefinition = { + id: 'test-server', + label: 'Test Server', + launch: { + type: McpServerTransportType.Stdio, + command: 'test-command', + args: [], + env: {}, + cwd: URI.parse('file:///test') + } + }; + }); + + function waitForHandler(cnx: McpServerConnection) { + const handler = cnx.handler.get(); + if (handler) { + return Promise.resolve(handler); + } + + return new Promise(resolve => { + const disposable = autorun(reader => { + const handler = cnx.handler.read(reader); + if (handler) { + disposable.dispose(); + resolve(handler); + } + }); + }); + } + + test('should start and set state to Running when transport succeeds', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch + ); + store.add(connection); + + // Start the connection + const startPromise = connection.start(); + + // Simulate successful connection + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + + const state = await startPromise; + assert.strictEqual(state.state, McpConnectionState.Kind.Running); + + transport.simulateInitialized(); + assert.ok(await waitForHandler(connection)); + }); + + test('should handle errors during start', async () => { + // Setup delegate to fail on start + delegate.setCanStart(false); + + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch + ); + store.add(connection); + + // Start the connection + const state = await connection.start(); + + assert.strictEqual(state.state, McpConnectionState.Kind.Error); + assert.ok(state.message); + }); + + test('should handle transport errors', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch + ); + store.add(connection); + + // Start the connection + const startPromise = connection.start(); + + // Simulate error in transport + transport.setConnectionState({ + state: McpConnectionState.Kind.Error, + message: 'Test error message' + }); + + const state = await startPromise; + assert.strictEqual(state.state, McpConnectionState.Kind.Error); + assert.strictEqual(state.message, 'Test error message'); + }); + + test('should stop and set state to Stopped', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch + ); + store.add(connection); + + // Start the connection + const startPromise = connection.start(); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + await startPromise; + + // Stop the connection + const stopPromise = connection.stop(); + await stopPromise; + + assert.strictEqual(connection.state.get().state, McpConnectionState.Kind.Stopped); + }); + + test('should not restart if already starting', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch + ); + store.add(connection); + + // Start the connection + const startPromise1 = connection.start(); + + // Try to start again while starting + const startPromise2 = connection.start(); + + // Simulate successful connection + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + + const state1 = await startPromise1; + const state2 = await startPromise2; + + // Both promises should resolve to the same state + assert.strictEqual(state1.state, McpConnectionState.Kind.Running); + assert.strictEqual(state2.state, McpConnectionState.Kind.Running); + + transport.simulateInitialized(); + assert.ok(await waitForHandler(connection)); + }); + + test('should clean up when disposed', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch + ); + + // Start the connection + const startPromise = connection.start(); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + await startPromise; + + // Dispose the connection + connection.dispose(); + + assert.strictEqual(connection.state.get().state, McpConnectionState.Kind.Stopped); + }); + + test('showOutput should call logger and output services', () => { + let channelShown = false; + const outputService = { + showChannel: (id: string) => { + assert.strictEqual(id, `mcpServer/${serverDefinition.id}`); + channelShown = true; + } + }; + + let loggerVisible = false; + const loggerService = new class extends TestLoggerService { + override setVisibility(id: string, visible: boolean): void { + assert.strictEqual(id, `mcpServer/${serverDefinition.id}`); + assert.strictEqual(visible, true); + loggerVisible = true; + } + }; + + // Override services + const services = new ServiceCollection( + [ILoggerService, store.add(loggerService)], + [IOutputService, upcast(outputService)], + [IStorageService, store.add(new TestStorageService())] + ); + + const localInstantiationService = store.add(new TestInstantiationService(services)); + + // Create server connection + const connection = localInstantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch + ); + store.add(connection); + + // Show output + connection.showOutput(); + + assert.strictEqual(channelShown, true); + assert.strictEqual(loggerVisible, true); + }); + + test('should log transport messages', async () => { + // Track logged messages + const loggedMessages: string[] = []; + const loggerService = new class extends TestLoggerService { + override createLogger(id: string) { + return { + info: (message: string) => { + loggedMessages.push(message); + }, + error: () => { }, + dispose: () => { } + } as Partial as ILogger; + } + }; + + // Override services + const services = new ServiceCollection( + [ILoggerService, store.add(loggerService)], + [IOutputService, upcast({ showChannel: () => { } })], + [IStorageService, store.add(new TestStorageService())] + ); + + const localInstantiationService = store.add(new TestInstantiationService(services)); + + // Create server connection + const connection = localInstantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch + ); + store.add(connection); + + // Start the connection + const startPromise = connection.start(); + + // Simulate log message from transport + transport.simulateLog('Test log message'); + + // Set connection to running + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + await startPromise; + + // Check that the message was logged + assert.ok(loggedMessages.some(msg => msg === 'Test log message')); + }); + + test('should correctly handle transitions to and from error state', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch + ); + store.add(connection); + + // Start the connection + const startPromise = connection.start(); + + // Transition to error state + const errorState: McpConnectionState = { + state: McpConnectionState.Kind.Error, + message: 'Temporary error' + }; + transport.setConnectionState(errorState); + + let state = await startPromise; + assert.equal(state, errorState); + + + transport.setConnectionState({ state: McpConnectionState.Kind.Stopped }); + + // Transition back to running state + const startPromise2 = connection.start(); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + state = await startPromise2; + assert.deepStrictEqual(state, { state: McpConnectionState.Kind.Running }); + + connection.dispose(); + await timeout(10); + }); + + test('should handle multiple start/stop cycles', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch + ); + store.add(connection); + + // First cycle + let startPromise = connection.start(); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + await startPromise; + + await connection.stop(); + assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Stopped }); + + // Second cycle + startPromise = connection.start(); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + await startPromise; + + assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Running }); + + await connection.stop(); + + assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Stopped }); + + connection.dispose(); + await timeout(10); + }); +}); diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts new file mode 100644 index 00000000000..ebbfeb75798 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts @@ -0,0 +1,396 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { upcast } from '../../../../../base/common/types.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILoggerService } from '../../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { IStorageService } from '../../../../../platform/storage/common/storage.js'; +import { TestLoggerService, TestProductService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { IMcpHostDelegate } from '../../common/mcpRegistryTypes.js'; +import { McpServerRequestHandler } from '../../common/mcpServerRequestHandler.js'; +import { McpConnectionState } from '../../common/mcpTypes.js'; +import { MCP } from '../../common/modelContextProtocol.js'; +import { TestMcpMessageTransport } from './mcpRegistryTypes.js'; +import { IOutputService } from '../../../../services/output/common/output.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; + +class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate { + private readonly _transport: TestMcpMessageTransport; + + constructor() { + super(); + this._transport = this._register(new TestMcpMessageTransport()); + } + + canStart(): boolean { + return true; + } + + start(): TestMcpMessageTransport { + return this._transport; + } + + getTransport(): TestMcpMessageTransport { + return this._transport; + } + + waitForInitialProviderPromises(): Promise { + return Promise.resolve(); + } +} + +suite('Workbench - MCP - ServerRequestHandler', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + let delegate: TestMcpHostDelegate; + let transport: TestMcpMessageTransport; + let handler: McpServerRequestHandler; + let cts: CancellationTokenSource; + + setup(async () => { + delegate = store.add(new TestMcpHostDelegate()); + transport = delegate.getTransport(); + cts = store.add(new CancellationTokenSource()); + + // Setup test services + const services = new ServiceCollection( + [ILoggerService, store.add(new TestLoggerService())], + [IOutputService, upcast({ showChannel: () => { } })], + [IStorageService, store.add(new TestStorageService())], + [IProductService, TestProductService], + ); + + instantiationService = store.add(new TestInstantiationService(services)); + + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + + // Manually create the handler since we need the transport already set up + const logger = store.add((instantiationService.get(ILoggerService) as TestLoggerService) + .createLogger('mcpServerTest', { hidden: true, name: 'MCP Test' })); + + // Start the handler creation + const handlerPromise = McpServerRequestHandler.create(instantiationService, transport, logger, cts.token); + + // Simulate successful initialization + // We need to respond to the initialize request that the handler will make + transport.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: 1, // The handler uses 1 for the first request + result: { + protocolVersion: MCP.LATEST_PROTOCOL_VERSION, + serverInfo: { + name: 'Test MCP Server', + version: '1.0.0', + }, + capabilities: { + resources: { + supportedTypes: ['text/plain'], + }, + tools: { + supportsCancellation: true, + } + } + } + }); + + handler = await handlerPromise; + store.add(handler); + }); + + test('should send and receive JSON-RPC requests', async () => { + // Setup request + const requestPromise = handler.listResources(); + + // Get the sent message and verify it + const sentMessages = transport.getSentMessages(); + assert.strictEqual(sentMessages.length, 3); // initialize + listResources + + // Verify listResources request format + const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest; + assert.strictEqual(listResourcesRequest.method, 'resources/list'); + assert.strictEqual(listResourcesRequest.jsonrpc, MCP.JSONRPC_VERSION); + assert.ok(typeof listResourcesRequest.id === 'number'); + + // Simulate server response with mock resources that match the expected Resource interface + transport.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: listResourcesRequest.id, + result: { + resources: [ + { uri: 'resource1', type: 'text/plain', name: 'Test Resource 1' }, + { uri: 'resource2', type: 'text/plain', name: 'Test Resource 2' } + ] + } + }); + + // Verify the result + const resources = await requestPromise; + assert.strictEqual(resources.length, 2); + assert.strictEqual(resources[0].uri, 'resource1'); + assert.strictEqual(resources[1].name, 'Test Resource 2'); + }); + + test('should handle paginated requests', async () => { + // Setup request + const requestPromise = handler.listResources(); + + // Get the first request and respond with pagination + const sentMessages = transport.getSentMessages(); + const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest; + + // Send first page with nextCursor + transport.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: listResourcesRequest.id, + result: { + resources: [ + { uri: 'resource1', type: 'text/plain', name: 'Test Resource 1' } + ], + nextCursor: 'page2' + } + }); + + // Clear the sent messages to only capture the next page request + transport.clearSentMessages(); + + // Wait a bit to allow the handler to process and send the next request + await new Promise(resolve => setTimeout(resolve, 0)); + + // Get the second request and verify cursor is included + const sentMessages2 = transport.getSentMessages(); + assert.strictEqual(sentMessages2.length, 1); + + const listResourcesRequest2 = sentMessages2[0] as MCP.JSONRPCRequest; + assert.strictEqual(listResourcesRequest2.method, 'resources/list'); + assert.deepStrictEqual(listResourcesRequest2.params, { cursor: 'page2' }); + + // Send final page with no nextCursor + transport.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: listResourcesRequest2.id, + result: { + resources: [ + { uri: 'resource2', type: 'text/plain', name: 'Test Resource 2' } + ] + } + }); + + // Verify the combined result + const resources = await requestPromise; + assert.strictEqual(resources.length, 2); + assert.strictEqual(resources[0].uri, 'resource1'); + assert.strictEqual(resources[1].uri, 'resource2'); + }); + + test('should handle error responses', async () => { + // Setup request + const requestPromise = handler.readResource({ uri: 'non-existent' }); + + // Get the sent message + const sentMessages = transport.getSentMessages(); + const readResourceRequest = sentMessages[2] as MCP.JSONRPCRequest; // [0] is initialize + + // Simulate error response + transport.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: readResourceRequest.id, + error: { + code: MCP.METHOD_NOT_FOUND, + message: 'Resource not found' + } + }); + + // Verify the error is thrown correctly + try { + await requestPromise; + assert.fail('Expected error was not thrown'); + } catch (e: any) { + assert.strictEqual(e.message, 'MPC -32601: Resource not found'); + assert.strictEqual(e.code, MCP.METHOD_NOT_FOUND); + } + }); + + test('should handle server requests', async () => { + // Simulate ping request from server + const pingRequest: MCP.JSONRPCRequest & MCP.PingRequest = { + jsonrpc: MCP.JSONRPC_VERSION, + id: 100, + method: 'ping' + }; + + transport.simulateReceiveMessage(pingRequest); + + // The handler should have sent a response + const sentMessages = transport.getSentMessages(); + const pingResponse = sentMessages.find(m => + 'id' in m && m.id === pingRequest.id && 'result' in m + ) as MCP.JSONRPCResponse; + + assert.ok(pingResponse, 'No ping response was sent'); + assert.deepStrictEqual(pingResponse.result, {}); + }); + + test('should handle roots list requests', async () => { + // Set roots + handler.roots = [ + { uri: 'file:///test/root1', name: 'Root 1' }, + { uri: 'file:///test/root2', name: 'Root 2' } + ]; + + // Simulate roots/list request from server + const rootsRequest: MCP.JSONRPCRequest & MCP.ListRootsRequest = { + jsonrpc: MCP.JSONRPC_VERSION, + id: 101, + method: 'roots/list' + }; + + transport.simulateReceiveMessage(rootsRequest); + + // The handler should have sent a response + const sentMessages = transport.getSentMessages(); + const rootsResponse = sentMessages.find(m => + 'id' in m && m.id === rootsRequest.id && 'result' in m + ) as MCP.JSONRPCResponse; + + assert.ok(rootsResponse, 'No roots/list response was sent'); + assert.strictEqual((rootsResponse.result as MCP.ListRootsResult).roots.length, 2); + assert.strictEqual((rootsResponse.result as MCP.ListRootsResult).roots[0].uri, 'file:///test/root1'); + }); + + test('should handle server notifications', async () => { + let progressNotificationReceived = false; + store.add(handler.onDidReceiveProgressNotification(notification => { + progressNotificationReceived = true; + assert.strictEqual(notification.method, 'notifications/progress'); + assert.strictEqual(notification.params.progressToken, 'token1'); + assert.strictEqual(notification.params.progress, 50); + })); + + // Simulate progress notification with correct format + const progressNotification: MCP.JSONRPCNotification & MCP.ProgressNotification = { + jsonrpc: MCP.JSONRPC_VERSION, + method: 'notifications/progress', + params: { + progressToken: 'token1', + progress: 50, + total: 100 + } + }; + + transport.simulateReceiveMessage(progressNotification); + assert.strictEqual(progressNotificationReceived, true); + }); + + test('should handle cancellation', async () => { + // Setup a new cancellation token source for this specific test + const testCts = store.add(new CancellationTokenSource()); + const requestPromise = handler.listResources(undefined, testCts.token); + + // Get the request ID + const sentMessages = transport.getSentMessages(); + const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest; + const requestId = listResourcesRequest.id; + + // Cancel the request + testCts.cancel(); + + // Check that a cancellation notification was sent + const cancelNotification = transport.getSentMessages().find(m => + !('id' in m) && + 'method' in m && + m.method === 'notifications/cancelled' && + 'params' in m && + m.params && m.params.requestId === requestId + ); + + assert.ok(cancelNotification, 'No cancellation notification was sent'); + + // Verify the promise was cancelled + try { + await requestPromise; + assert.fail('Promise should have been cancelled'); + } catch (e) { + assert.strictEqual(e.name, 'Canceled'); + } + }); + + test('should handle cancelled notification from server', async () => { + // Setup request + const requestPromise = handler.listResources(); + + // Get the request ID + const sentMessages = transport.getSentMessages(); + const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest; + const requestId = listResourcesRequest.id; + + // Simulate cancelled notification from server + const cancelledNotification: MCP.JSONRPCNotification & MCP.CancelledNotification = { + jsonrpc: MCP.JSONRPC_VERSION, + method: 'notifications/cancelled', + params: { + requestId + } + }; + + transport.simulateReceiveMessage(cancelledNotification); + + // Verify the promise was cancelled + try { + await requestPromise; + assert.fail('Promise should have been cancelled'); + } catch (e) { + assert.strictEqual(e.name, 'Canceled'); + } + }); + + test('should dispose properly and cancel pending requests', async () => { + // Setup multiple requests + const request1 = handler.listResources(); + const request2 = handler.listTools(); + + // Dispose the handler + handler.dispose(); + + // Verify all promises were cancelled + try { + await request1; + assert.fail('Promise 1 should have been cancelled'); + } catch (e) { + assert.strictEqual(e.name, 'Canceled'); + } + + try { + await request2; + assert.fail('Promise 2 should have been cancelled'); + } catch (e) { + assert.strictEqual(e.name, 'Canceled'); + } + }); + + test('should handle connection error by cancelling requests', async () => { + // Setup request + const requestPromise = handler.listResources(); + + // Simulate connection error + transport.setConnectionState({ + state: McpConnectionState.Kind.Error, + message: 'Connection lost' + }); + + // Verify the promise was cancelled + try { + await requestPromise; + assert.fail('Promise should have been cancelled'); + } catch (e) { + assert.strictEqual(e.name, 'Canceled'); + } + }); +}); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index d60d64bcb8d..308db886d5b 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -5,7 +5,7 @@ import { ArrayQueue, CompareResult } from '../../../../base/common/arrays.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; -import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable, autorunOpts } from '../../../../base/common/observable.js'; import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; import { IModelDeltaDecoration } from '../../../../editor/common/model.js'; @@ -85,19 +85,6 @@ export function elementAtOrUndefined(arr: T[], index: number): T | undefined return arr[index]; } -export function thenIfNotDisposed(promise: Promise, then: () => void): IDisposable { - let disposed = false; - promise.then(() => { - if (disposed) { - return; - } - then(); - }); - return toDisposable(() => { - disposed = true; - }); -} - export function setFields(obj: T, fields: Partial): T { return Object.assign(obj, fields); } @@ -154,4 +141,3 @@ export class PersistentStore { ); } } - diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 32b12772bef..666ad3f373e 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -10,7 +10,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Color } from '../../../../../base/common/color.js'; import { BugIndicatingError, onUnexpectedError } from '../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, thenIfNotDisposed, toDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun, autorunWithStore, IObservable, IReader, observableValue, transaction } from '../../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../../base/common/resources.js'; import { isDefined } from '../../../../../base/common/types.js'; @@ -23,7 +23,6 @@ import { ICodeEditorViewState, ScrollType } from '../../../../../editor/common/e import { ITextModel } from '../../../../../editor/common/model.js'; import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js'; import { localize } from '../../../../../nls.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IEditorOptions, ITextEditorOptions, ITextResourceEditorInput } from '../../../../../platform/editor/common/editor.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; @@ -39,8 +38,7 @@ import { readTransientState, writeTransientState } from '../../../codeEditor/bro import { MergeEditorInput } from '../mergeEditorInput.js'; import { IMergeEditorInputModel } from '../mergeEditorInputModel.js'; import { MergeEditorModel } from '../model/mergeEditorModel.js'; -import { deepMerge, PersistentStore, thenIfNotDisposed } from '../utils.js'; -import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { deepMerge, PersistentStore } from '../utils.js'; import { BaseCodeEditorView } from './editors/baseCodeEditorView.js'; import { ScrollSynchronizer } from './scrollSynchronizer.js'; import { MergeEditorViewModel } from './viewModel.js'; @@ -90,22 +88,12 @@ export class MergeEditor extends AbstractTextEditor { return this.inputModel.get()?.model; } - private get inputsWritable(): boolean { - return !!this._configurationService.getValue('mergeEditor.writableInputs'); - } - private readonly viewZoneComputer = new ViewZoneComputer( this.input1View.editor, this.input2View.editor, this.inputResultView.editor, ); - protected readonly codeLensesVisible = observableConfigValue( - 'mergeEditor.showCodeLenses', - true, - this.configurationService, - ); - private readonly scrollSynchronizer = this._register(new ScrollSynchronizer(this._viewModel, this.input1View, this.input2View, this.baseView, this.inputResultView, this._layoutModeObs)); constructor( @@ -116,12 +104,10 @@ export class MergeEditor extends AbstractTextEditor { @IStorageService storageService: IStorageService, @IThemeService themeService: IThemeService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @IEditorService editorService: IEditorService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IFileService fileService: IFileService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService ) { super(MergeEditor.ID, group, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } @@ -170,14 +156,18 @@ export class MergeEditor extends AbstractTextEditor { const inputOptions: ICodeEditorOptions = deepMerge(options, { minimap: { enabled: false }, glyphMargin: false, - lineNumbersMinChars: 2, - readOnly: !this.inputsWritable + lineNumbersMinChars: 2 }); - this.input1View.updateOptions(inputOptions); - this.input2View.updateOptions(inputOptions); + const readOnlyInputOptions: ICodeEditorOptions = deepMerge(inputOptions, { + readOnly: true, + readOnlyMessage: undefined + }); + + this.input1View.updateOptions(readOnlyInputOptions); + this.input2View.updateOptions(readOnlyInputOptions); this.baseViewOptions.set({ ...this.input2View.editor.getRawOptions() }, undefined); - this.inputResultView.updateOptions(options); + this.inputResultView.updateOptions(inputOptions); } protected getMainControl(): ICodeEditor | undefined { @@ -213,7 +203,6 @@ export class MergeEditor extends AbstractTextEditor { this.showNonConflictingChanges, ); - model.telemetry.reportMergeEditorOpened({ combinableConflictCount: model.combinableConflictCount, conflictCount: model.conflictCount, @@ -382,7 +371,7 @@ export class MergeEditor extends AbstractTextEditor { const resultViewZoneIds: string[] = []; const viewZones = this.viewZoneComputer.computeViewZones(reader, viewModel, { - codeLensesVisible: this.codeLensesVisible.read(reader), + codeLensesVisible: true, showNonConflictingChanges: this.showNonConflictingChanges.read(reader), shouldAlignBase, shouldAlignResult, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts index 00b45c45d82..2fec25ce46f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts @@ -11,10 +11,9 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { CellKind, NotebookSetting } from '../../../common/notebookCommon.js'; import { INotebookEditor, INotebookEditorContribution } from '../../notebookBrowser.js'; import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; -import { Iterable } from '../../../../../../base/common/iterator.js'; import { CodeCellViewModel } from '../../viewModel/codeCellViewModel.js'; import { Event } from '../../../../../../base/common/event.js'; -import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; +import { ChatAgentLocation, IChatAgentService } from '../../../../chat/common/chatAgents.js'; export class CellDiagnostics extends Disposable implements INotebookEditorContribution { @@ -43,12 +42,17 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri })); } + private hasNotebookAgent(): boolean { + const agents = this.chatAgentService.getAgents(); + return !!agents.find(agent => agent.locations.includes(ChatAgentLocation.Notebook)); + } + private updateEnabled() { const settingEnabled = this.configurationService.getValue(NotebookSetting.cellFailureDiagnostics); - if (this.enabled && (!settingEnabled || Iterable.isEmpty(this.chatAgentService.getAgents()))) { + if (this.enabled && (!settingEnabled || !this.hasNotebookAgent())) { this.enabled = false; this.clearAll(); - } else if (!this.enabled && settingEnabled && !Iterable.isEmpty(this.chatAgentService.getAgents())) { + } else if (!this.enabled && settingEnabled && this.hasNotebookAgent()) { this.enabled = true; if (!this.listening) { this.listening = true; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts index 0d7c0c3d00f..b86b3cdeb68 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts @@ -15,8 +15,7 @@ import { registerNotebookContribution } from '../../notebookEditorExtensions.js' import { CodeCellViewModel } from '../../viewModel/codeCellViewModel.js'; import { INotebookCellStatusBarItem, CellStatusbarAlignment } from '../../../common/notebookCommon.js'; import { ICellExecutionError } from '../../../common/notebookExecutionStateService.js'; -import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; -import { Iterable } from '../../../../../../base/common/iterator.js'; +import { ChatAgentLocation, IChatAgentService } from '../../../../chat/common/chatAgents.js'; export class DiagnosticCellStatusBarContrib extends Disposable implements INotebookEditorContribution { static id: string = 'workbench.notebook.statusBar.diagtnostic'; @@ -49,10 +48,15 @@ class DiagnosticCellStatusBarItem extends Disposable { this._register(autorun((reader) => this.updateSparkleItem(reader.readObservable(cell.executionErrorDiagnostic)))); } + private hasNotebookAgent(): boolean { + const agents = this.chatAgentService.getAgents(); + return !!agents.find(agent => agent.locations.includes(ChatAgentLocation.Notebook)); + } + private async updateSparkleItem(error: ICellExecutionError | undefined) { let item: INotebookCellStatusBarItem | undefined; - if (error?.location && !Iterable.isEmpty(this.chatAgentService.getAgents())) { + if (error?.location && this.hasNotebookAgent()) { const keybinding = this.keybindingService.lookupKeybinding(OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID)?.getLabel(); const tooltip = localize('notebook.cell.status.diagnostic', "Quick Actions {0}", `(${keybinding})`); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts index 992fcddbbaa..184c94dbaac 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts @@ -288,7 +288,7 @@ class TimerCellStatusBarItem extends Disposable { this._deferredUpdate = disposableTimeout(() => { this._deferredUpdate = undefined; this._currentItemIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); - }, UPDATE_TIMER_GRACE_PERIOD); + }, UPDATE_TIMER_GRACE_PERIOD, this._store); } } else { this._deferredUpdate?.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/contribution.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/contribution.ts deleted file mode 100644 index b8123bc8c09..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/contribution.ts +++ /dev/null @@ -1,21 +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 { INotebookModelSynchronizerFactory, NotebookModelSynchronizerFactory } from './notebookSynchronizer.js'; -import { INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceFactory } from './notebookOriginalModelRefFactory.js'; -import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; -import { InstantiationType, registerSingleton } from '../../../../../../platform/instantiation/common/extensions.js'; -import { INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory } from './notebookOriginalCellModelFactory.js'; -import { NotebookChatEditorControllerContrib } from './notebookChatEditController.js'; -import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../common/contributions.js'; -import { ChatEditingNotebookFileSystemProviderContrib } from './chatEditingNotebookFileSystemProvider.js'; - - -registerNotebookContribution(NotebookChatEditorControllerContrib.ID, NotebookChatEditorControllerContrib); -registerSingleton(INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceFactory, InstantiationType.Delayed); -registerSingleton(INotebookModelSynchronizerFactory, NotebookModelSynchronizerFactory, InstantiationType.Delayed); -registerSingleton(INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory, InstantiationType.Delayed); - -registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts deleted file mode 100644 index ec0d2f43511..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts +++ /dev/null @@ -1,361 +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 { DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, derived, observableFromEvent } from '../../../../../../base/common/observable.js'; -import { IChatEditingService, ChatEditingSessionState } from '../../../../chat/common/chatEditingService.js'; -import { INotebookEditor } from '../../notebookBrowser.js'; -import { ThrottledDelayer } from '../../../../../../base/common/async.js'; -import { ICodeEditor, IViewZone } from '../../../../../../editor/browser/editorBrowser.js'; -import { IEditorWorkerService } from '../../../../../../editor/common/services/editorWorker.js'; -import { EditorOption } from '../../../../../../editor/common/config/editorOptions.js'; -import { themeColorFromId } from '../../../../../../base/common/themables.js'; -import { RenderOptions, LineSource, renderLines } from '../../../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; -import { diffAddDecoration, diffWholeLineAddDecoration, diffDeleteDecoration } from '../../../../../../editor/browser/widget/diffEditor/registrations.contribution.js'; -import { IDocumentDiff } from '../../../../../../editor/common/diff/documentDiffProvider.js'; -import { ITextModel, TrackedRangeStickiness, MinimapPosition, IModelDeltaDecoration, OverviewRulerLane } from '../../../../../../editor/common/model.js'; -import { ModelDecorationOptions } from '../../../../../../editor/common/model/textModel.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../../../editor/common/viewModel.js'; -import { Range } from '../../../../../../editor/common/core/range.js'; -import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; -import { INotebookOriginalCellModelFactory } from './notebookOriginalCellModelFactory.js'; -import { DetailedLineRangeMapping } from '../../../../../../editor/common/diff/rangeMapping.js'; -import { isEqual } from '../../../../../../base/common/resources.js'; -import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../../../scm/common/quickDiff.js'; - - -export class NotebookCellDiffDecorator extends DisposableStore { - private _viewZones: string[] = []; - private readonly throttledDecorator = new ThrottledDelayer(50); - private diffForPreviouslyAppliedDecorators?: IDocumentDiff; - - private readonly perEditorDisposables = this.add(new DisposableStore()); - constructor( - notebookEditor: INotebookEditor, - public readonly modifiedCell: NotebookCellTextModel, - public readonly originalCell: NotebookCellTextModel, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @INotebookOriginalCellModelFactory private readonly originalCellModelFactory: INotebookOriginalCellModelFactory, - - ) { - super(); - - const onDidChangeVisibleRanges = observableFromEvent(notebookEditor.onDidChangeVisibleRanges, () => notebookEditor.visibleRanges); - const editorObs = derived((r) => { - const visibleRanges = onDidChangeVisibleRanges.read(r); - const visibleCellHandles = visibleRanges.map(range => notebookEditor.getCellsInRange(range)).flat().map(c => c.handle); - if (!visibleCellHandles.includes(modifiedCell.handle)) { - return; - } - const editor = notebookEditor.codeEditors.find(item => item[0].handle === modifiedCell.handle)?.[1]; - if (editor?.getModel() !== this.modifiedCell.textModel) { - return; - } - return editor; - }); - - this.add(autorunWithStore((r, store) => { - const editor = editorObs.read(r); - this.perEditorDisposables.clear(); - - if (editor) { - store.add(editor.onDidChangeModel(() => { - this.perEditorDisposables.clear(); - })); - store.add(editor.onDidChangeModelContent(() => { - this.update(editor); - })); - store.add(editor.onDidChangeConfiguration((e) => { - if (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.lineHeight)) { - this.update(editor); - } - })); - this.update(editor); - } - })); - - const shouldBeReadOnly = derived(this, r => { - const editorUri = editorObs.read(r)?.getModel()?.uri; - if (!editorUri) { - return false; - } - const sessions = this._chatEditingService.editingSessionsObs.read(r); - const session = sessions.find(s => s.entries.read(r).some(e => isEqual(e.modifiedURI, editorUri))); - return session?.state.read(r) === ChatEditingSessionState.StreamingEdits; - }); - - - let actualReadonly: boolean | undefined; - let actualDeco: 'off' | 'editable' | 'on' | undefined; - - this.add(autorun((r) => { - const editor = editorObs.read(r); - if (!editor) { - return; - } - const value = shouldBeReadOnly.read(r); - if (value) { - actualReadonly ??= editor.getOption(EditorOption.readOnly); - actualDeco ??= editor.getOption(EditorOption.renderValidationDecorations); - - editor.updateOptions({ - readOnly: true, - renderValidationDecorations: 'off' - }); - } else { - if (actualReadonly !== undefined && actualDeco !== undefined) { - editor.updateOptions({ - readOnly: actualReadonly, - renderValidationDecorations: actualDeco - }); - actualReadonly = undefined; - actualDeco = undefined; - } - } - })); - } - - public update(editor: ICodeEditor): void { - this.throttledDecorator.trigger(() => this._updateImpl(editor)); - } - - private async _updateImpl(editor: ICodeEditor) { - if (this.isDisposed) { - return; - } - if (editor.getOption(EditorOption.inDiffEditor)) { - this.perEditorDisposables.clear(); - return; - } - const model = editor.getModel(); - if (!model || model !== this.modifiedCell.textModel) { - this.perEditorDisposables.clear(); - return; - } - - const originalModel = this.getOrCreateOriginalModel(editor); - if (!originalModel) { - this.perEditorDisposables.clear(); - return; - } - const version = model.getVersionId(); - const diff = await this._editorWorkerService.computeDiff( - originalModel.uri, - model.uri, - { computeMoves: true, ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER }, - 'advanced' - ); - - - if (this.isDisposed) { - return; - } - - - if (diff && !diff.identical && originalModel && model === editor.getModel() && editor.getModel()?.getVersionId() === version) { - this._updateWithDiff(editor, originalModel, diff); - } else { - this.perEditorDisposables.clear(); - } - } - - private _originalModel?: ITextModel; - private getOrCreateOriginalModel(editor: ICodeEditor) { - if (!this._originalModel) { - const model = editor.getModel(); - if (!model) { - return; - } - this._originalModel = this.add(this.originalCellModelFactory.getOrCreate(model.uri, this.originalCell.getValue(), model.getLanguageId(), this.modifiedCell.cellKind)).object; - } - return this._originalModel; - } - - private _updateWithDiff(editor: ICodeEditor, originalModel: ITextModel | undefined, diff: IDocumentDiff): void { - if (areDiffsEqual(diff, this.diffForPreviouslyAppliedDecorators)) { - return; - } - this.perEditorDisposables.clear(); - const decorations = editor.createDecorationsCollection(); - this.perEditorDisposables.add(toDisposable(() => { - editor.changeViewZones((viewZoneChangeAccessor) => { - for (const id of this._viewZones) { - viewZoneChangeAccessor.removeZone(id); - } - }); - this._viewZones = []; - decorations.clear(); - this.diffForPreviouslyAppliedDecorators = undefined; - })); - - this.diffForPreviouslyAppliedDecorators = diff; - - const chatDiffAddDecoration = ModelDecorationOptions.createDynamic({ - ...diffAddDecoration, - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges - }); - const chatDiffWholeLineAddDecoration = ModelDecorationOptions.createDynamic({ - ...diffWholeLineAddDecoration, - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - }); - const createOverviewDecoration = (overviewRulerColor: string, minimapColor: string) => { - return ModelDecorationOptions.createDynamic({ - description: 'chat-editing-decoration', - overviewRuler: { color: themeColorFromId(overviewRulerColor), position: OverviewRulerLane.Left }, - minimap: { color: themeColorFromId(minimapColor), position: MinimapPosition.Gutter }, - }); - }; - const modifiedDecoration = createOverviewDecoration(overviewRulerModifiedForeground, minimapGutterModifiedBackground); - const addedDecoration = createOverviewDecoration(overviewRulerAddedForeground, minimapGutterAddedBackground); - const deletedDecoration = createOverviewDecoration(overviewRulerDeletedForeground, minimapGutterDeletedBackground); - - editor.changeViewZones((viewZoneChangeAccessor) => { - for (const id of this._viewZones) { - viewZoneChangeAccessor.removeZone(id); - } - this._viewZones = []; - const modifiedDecorations: IModelDeltaDecoration[] = []; - const mightContainNonBasicASCII = originalModel?.mightContainNonBasicASCII(); - const mightContainRTL = originalModel?.mightContainRTL(); - const renderOptions = RenderOptions.fromEditor(editor); - - for (const diffEntry of diff.changes) { - const originalRange = diffEntry.original; - if (originalModel) { - originalModel.tokenization.forceTokenization(Math.max(1, originalRange.endLineNumberExclusive - 1)); - } - const source = new LineSource( - (originalRange.length && originalModel) ? originalRange.mapToLineArray(l => originalModel.tokenization.getLineTokens(l)) : [], - [], - mightContainNonBasicASCII, - mightContainRTL, - ); - const decorations: InlineDecoration[] = []; - for (const i of diffEntry.innerChanges || []) { - decorations.push(new InlineDecoration( - i.originalRange.delta(-(diffEntry.original.startLineNumber - 1)), - diffDeleteDecoration.className!, - InlineDecorationType.Regular - )); - modifiedDecorations.push({ - range: i.modifiedRange, options: chatDiffAddDecoration - }); - } - if (!diffEntry.modified.isEmpty) { - modifiedDecorations.push({ - range: diffEntry.modified.toInclusiveRange()!, options: chatDiffWholeLineAddDecoration - }); - } - - if (diffEntry.original.isEmpty) { - // insertion - modifiedDecorations.push({ - range: diffEntry.modified.toInclusiveRange()!, - options: addedDecoration - }); - } else if (diffEntry.modified.isEmpty) { - // deletion - modifiedDecorations.push({ - range: new Range(diffEntry.modified.startLineNumber - 1, 1, diffEntry.modified.startLineNumber, 1), - options: deletedDecoration - }); - } else { - // modification - modifiedDecorations.push({ - range: diffEntry.modified.toInclusiveRange()!, - options: modifiedDecoration - }); - } - const domNode = document.createElement('div'); - domNode.className = 'chat-editing-original-zone view-lines line-delete monaco-mouse-cursor-text'; - const result = renderLines(source, renderOptions, decorations, domNode); - - const isCreatedContent = decorations.length === 1 && decorations[0].range.isEmpty() && decorations[0].range.startLineNumber === 1; - if (!isCreatedContent) { - const viewZoneData: IViewZone = { - afterLineNumber: diffEntry.modified.startLineNumber - 1, - heightInLines: result.heightInLines, - domNode, - ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42 - }; - - this._viewZones.push(viewZoneChangeAccessor.addZone(viewZoneData)); - } - } - - decorations.set(modifiedDecorations); - }); - } -} - -function areDiffsEqual(a: IDocumentDiff | undefined, b: IDocumentDiff | undefined): boolean { - if (a && b) { - if (a.changes.length !== b.changes.length) { - return false; - } - if (a.moves.length !== b.moves.length) { - return false; - } - if (!areLineRangeMappinsEqual(a.changes, b.changes)) { - return false; - } - if (!a.moves.some((move, i) => { - const bMove = b.moves[i]; - if (!areLineRangeMappinsEqual(move.changes, bMove.changes)) { - return true; - } - if (move.lineRangeMapping.changedLineCount !== bMove.lineRangeMapping.changedLineCount) { - return true; - } - if (!move.lineRangeMapping.modified.equals(bMove.lineRangeMapping.modified)) { - return true; - } - if (!move.lineRangeMapping.original.equals(bMove.lineRangeMapping.original)) { - return true; - } - return false; - })) { - return false; - } - return true; - } else if (!a && !b) { - return true; - } else { - return false; - } -} - -function areLineRangeMappinsEqual(a: readonly DetailedLineRangeMapping[], b: readonly DetailedLineRangeMapping[]): boolean { - if (a.length !== b.length) { - return false; - } - if (a.some((c, i) => { - const bChange = b[i]; - if (c.changedLineCount !== bChange.changedLineCount) { - return true; - } - if ((c.innerChanges || []).length !== (bChange.innerChanges || []).length) { - return true; - } - if ((c.innerChanges || []).some((innerC, innerIdx) => { - const bInnerC = bChange.innerChanges![innerIdx]; - if (!innerC.modifiedRange.equalsRange(bInnerC.modifiedRange)) { - return true; - } - if (!innerC.originalRange.equalsRange(bInnerC.originalRange)) { - return true; - } - return false; - })) { - return true; - } - - return false; - })) { - return false; - } - return true; -} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts deleted file mode 100644 index 5669e5b1ed9..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts +++ /dev/null @@ -1,323 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { CellEditState, getNotebookEditorFromEditorPane, INotebookEditor, INotebookViewModel } from '../../notebookBrowser.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; -import { MenuId } from '../../../../../../platform/actions/common/actions.js'; -import { ActionViewItem } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { ActionRunner, IAction, IActionRunner } from '../../../../../../base/common/actions.js'; -import { $ } from '../../../../../../base/browser/dom.js'; -import { IChatEditingService, IModifiedFileEntry } from '../../../../chat/common/chatEditingService.js'; -import { ACTIVE_GROUP, IEditorService } from '../../../../../services/editor/common/editorService.js'; -import { autorun, autorunWithStore, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; -import { isEqual } from '../../../../../../base/common/resources.js'; -import { CellDiffInfo } from '../../diff/notebookDiffViewModel.js'; -import { AcceptAction, navigationBearingFakeActionId, RejectAction } from '../../../../chat/browser/chatEditing/chatEditingEditorActions.js'; -import { INotebookDeletedCellDecorator } from '../../diff/inlineDiff/notebookDeletedCellDecorator.js'; -import { ChatEditingModifiedDocumentEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedDocumentEntry.js'; - -export class NotebookChatActionsOverlayController extends Disposable { - constructor( - private readonly notebookEditor: INotebookEditor, - cellDiffInfo: IObservable, - deletedCellDecorator: INotebookDeletedCellDecorator, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - - const notebookModel = observableFromEvent(this.notebookEditor.onDidChangeModel, e => e); - - this._register(autorunWithStore((r, store) => { - const model = notebookModel.read(r); - if (!model) { - return; - } - const sessions = this._chatEditingService.editingSessionsObs.read(r); - const session = sessions.find(s => s.readEntry(model.uri, r)); - const entry = session?.readEntry(model.uri, r); - if (!session || !entry || !(entry instanceof ChatEditingModifiedDocumentEntry)) { - return; - } - - const entries = session.entries.read(r); - const idx = entries.findIndex(e => isEqual(e.modifiedURI, model.uri)); - if (idx >= 0) { - const entry = entries[idx]; - const nextEntry = entries[(idx + 1) % entries.length]; - const previousEntry = entries[(idx - 1 + entries.length) % entries.length]; - store.add(instantiationService.createInstance(NotebookChatActionsOverlay, notebookEditor, entry, cellDiffInfo, nextEntry, previousEntry, deletedCellDecorator)); - } - })); - } -} - -// Copied from src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts (until we unify these) -export class NotebookChatActionsOverlay extends Disposable { - private readonly focusedDiff = observableValue('focusedDiff', undefined); - private readonly toolbarNode: HTMLElement; - private added: boolean = false; - constructor( - private readonly notebookEditor: INotebookEditor, - entry: IModifiedFileEntry, - cellDiffInfo: IObservable, - nextEntry: IModifiedFileEntry, - previousEntry: IModifiedFileEntry, - deletedCellDecorator: INotebookDeletedCellDecorator, - @IEditorService _editorService: IEditorService, - @IInstantiationService instaService: IInstantiationService, - ) { - super(); - - this._register(notebookEditor.onDidBlurWidget(() => { - if (getNotebookEditorFromEditorPane(_editorService.activeEditorPane) !== notebookEditor) { - this.focusedDiff.set(undefined, undefined); - } - })); - this._register(notebookEditor.onDidChangeActiveEditor(e => { - if (e !== notebookEditor) { - this.focusedDiff.set(undefined, undefined); - } - })); - - this.toolbarNode = $('div'); - this.toolbarNode.classList.add('notebook-chat-editor-overlay-widget'); - this._register(toDisposable(() => this.hide())); - - this._register(autorun(r => { - const diffs = cellDiffInfo.read(r); - if (diffs?.some(d => d.type !== 'unchanged')) { - this.show(); - } else { - this.hide(); - } - })); - - const focusedDiff = this.focusedDiff; - const _toolbar = instaService.createInstance(MenuWorkbenchToolBar, this.toolbarNode, MenuId.ChatEditingEditorContent, { - telemetrySource: 'chatEditor.overlayToolbar', - hiddenItemStrategy: HiddenItemStrategy.Ignore, - toolbarOptions: { - primaryGroup: () => true, - useSeparatorsInPrimaryActions: true - }, - menuOptions: { renderShortTitle: true }, - actionViewItemProvider: (action, options) => { - - if (action.id === navigationBearingFakeActionId) { - return this._register(new class extends ActionViewItem { - constructor() { - super(undefined, action, { ...options, icon: false, label: false, keybindingNotRenderedWithLabel: true }); - } - }); - } - - if (action.id === AcceptAction.ID || action.id === RejectAction.ID) { - return this._register(new class extends ActionViewItem { - private readonly _reveal = this._store.add(new MutableDisposable()); - constructor() { - super(undefined, action, { ...options, icon: false, label: true, keybindingNotRenderedWithLabel: true }); - } - override set actionRunner(actionRunner: IActionRunner) { - super.actionRunner = actionRunner; - - const store = new DisposableStore(); - - store.add(actionRunner.onWillRun(_e => { - notebookEditor.focus(); - })); - store.add(actionRunner.onDidRun(e => { - if (e.action !== this.action) { - return; - } - if (entry === nextEntry) { - return; - } - - })); - - this._reveal.value = store; - } - override get actionRunner(): IActionRunner { - return super.actionRunner; - } - }); - } - // Override next/previous with our implementation. - if (action.id === 'chatEditor.action.navigateNext' || action.id === 'chatEditor.action.navigatePrevious') { - return this._register(new class extends ActionViewItem { - constructor() { - super(undefined, action, { ...options, icon: true, label: false, keybindingNotRenderedWithLabel: true }); - } - override set actionRunner(_: IActionRunner) { - const next = action.id === 'chatEditor.action.navigateNext' ? nextEntry : previousEntry; - const direction = action.id === 'chatEditor.action.navigateNext' ? 'next' : 'previous'; - super.actionRunner = this._register(new NextPreviousChangeActionRunner(notebookEditor, cellDiffInfo, entry, next, direction, _editorService, deletedCellDecorator, focusedDiff)); - } - override get actionRunner(): IActionRunner { - return super.actionRunner; - } - }); - } - return undefined; - } - - }); - - this._register(_toolbar); - } - - private show() { - if (!this.added) { - this.notebookEditor.getDomNode().appendChild(this.toolbarNode); - this.added = true; - } - } - - private hide() { - if (this.added) { - this.notebookEditor.getDomNode().removeChild(this.toolbarNode); - this.added = false; - } - } - - -} - -class NextPreviousChangeActionRunner extends ActionRunner { - constructor( - private readonly notebookEditor: INotebookEditor, - private readonly cellDiffInfo: IObservable, - private readonly entry: IModifiedFileEntry, - private readonly next: IModifiedFileEntry, - private readonly direction: 'next' | 'previous', - private readonly editorService: IEditorService, - private readonly deletedCellDecorator: INotebookDeletedCellDecorator, - private readonly focusedDiff: ISettableObservable - ) { - super(); - } - protected override async runAction(_action: IAction, _context?: unknown): Promise { - const viewModel = this.notebookEditor.getViewModel(); - const activeCell = this.notebookEditor.activeCellAndCodeEditor; - const cellDiff = this.cellDiffInfo.read(undefined); - if (!viewModel || !cellDiff?.length || (!activeCell && this.focusedDiff.read(undefined))) { - return this.goToNextEntry(); - } - - const nextDiff = this.getNextCellDiff(cellDiff, viewModel); - if (nextDiff && (await this.focusDiff(nextDiff, viewModel))) { - return; - } - - return this.goToNextEntry(); - } - - /** - * @returns `true` if focused to the next diff - */ - private async focusDiff(diff: CellDiffInfo, viewModel: INotebookViewModel) { - if (diff.type === 'delete') { - const top = this.deletedCellDecorator.getTop(diff.originalCellIndex); - if (typeof top === 'number') { - this.focusedDiff.set(diff, undefined); - this.notebookEditor.setScrollTop(top); - return true; - } - } else { - const index = diff.modifiedCellIndex; - this.focusedDiff.set(diff, undefined); - await this.notebookEditor.focusNotebookCell(viewModel.viewCells[index], 'container'); - this.notebookEditor.revealInViewAtTop(viewModel.viewCells[index]); - viewModel.viewCells[index].updateEditState(CellEditState.Editing, 'chatEdit'); - return true; - } - return false; - } - - private getNextCellDiff(cellDiffInfo: CellDiffInfo[], viewModel: INotebookViewModel) { - const activeCell = this.notebookEditor.activeCellAndCodeEditor; - const currentCellIndex = activeCell ? viewModel.viewCells.findIndex(c => c.handle === activeCell[0].handle) : (this.direction === 'next' ? 0 : viewModel.viewCells.length - 1); - if (this.focusedDiff.read(undefined)) { - const changes = cellDiffInfo.filter(d => d.type !== 'unchanged'); - const idx = changes.findIndex(d => d === this.focusedDiff.read(undefined)); - if (idx >= 0) { - const next = this.direction === 'next' ? idx + 1 : idx - 1; - if (next >= 0 && next < changes.length) { - return changes[next]; - } - } - } else if (this.direction === 'next') { - let currentIndex = 0; - let next: CellDiffInfo | undefined; - cellDiffInfo - .forEach((d, i) => { - if (next) { - return; - } - if (d.type === 'insert' || d.type === 'modified') { - if (d.modifiedCellIndex > currentCellIndex) { - next = d; - } - currentIndex = d.modifiedCellIndex; - } else if (d.type === 'unchanged') { - currentIndex = d.modifiedCellIndex; - } else if (currentIndex >= currentCellIndex) { - next = d; - } - }); - if (next) { - return next; - } - } else { - let currentIndex = 0; - let previous: CellDiffInfo | undefined; - cellDiffInfo - .forEach((d, i) => { - if (d.type === 'insert' || d.type === 'modified') { - if (d.modifiedCellIndex < currentCellIndex) { - previous = d; - } - currentIndex = d.modifiedCellIndex; - } else if (d.type === 'unchanged') { - currentIndex = d.modifiedCellIndex; - } else if (currentIndex <= currentCellIndex) { - previous = d; - } - }); - if (previous) { - return previous; - } - } - - if (this.canGoToNextEntry()) { - return; - } - - return this.direction === 'next' ? cellDiffInfo[0] : cellDiffInfo[cellDiffInfo.length - 1]; - } - - - private canGoToNextEntry() { - return this.entry !== this.next; - } - - private async goToNextEntry() { - if (!this.canGoToNextEntry()) { - return; - } - // For now just go to next/previous file. - this.focusedDiff.set(undefined, undefined); - await this.editorService.openEditor({ - resource: this.next.modifiedURI, - options: { - revealIfOpened: false, - revealIfVisible: false, - } - }, ACTIVE_GROUP); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts deleted file mode 100644 index 32cce7d9685..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts +++ /dev/null @@ -1,211 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, dispose, IReference, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { autorun, derived, derivedWithStore, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; -import { IChatEditingService, WorkingSetEntryState } from '../../../../chat/common/chatEditingService.js'; -import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; -import { INotebookEditor, INotebookEditorContribution } from '../../notebookBrowser.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { NotebookCellDiffDecorator } from './notebookCellDecorators.js'; -import { INotebookModelSynchronizerFactory, NotebookModelSynchronizer } from './notebookSynchronizer.js'; -import { INotebookOriginalModelReferenceFactory } from './notebookOriginalModelRefFactory.js'; -import { debouncedObservable } from '../../../../../../base/common/observableInternal/utils.js'; -import { CellDiffInfo } from '../../diff/notebookDiffViewModel.js'; -import { NotebookChatActionsOverlayController } from './notebookChatActionsOverlay.js'; -import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; -import { Event } from '../../../../../../base/common/event.js'; -import { ctxNotebookHasEditorModification } from './notebookChatEditContext.js'; -import { NotebookDeletedCellDecorator } from '../../diff/inlineDiff/notebookDeletedCellDecorator.js'; -import { NotebookInsertedCellDecorator } from '../../diff/inlineDiff/notebookInsertedCellDecorator.js'; -import { ChatEditingModifiedDocumentEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedDocumentEntry.js'; - -export class NotebookChatEditorControllerContrib extends Disposable implements INotebookEditorContribution { - - public static readonly ID: string = 'workbench.notebook.chatEditorController'; - readonly _serviceBrand: undefined; - constructor( - notebookEditor: INotebookEditor, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, - - ) { - super(); - this._register(instantiationService.createInstance(NotebookChatEditorController, notebookEditor)); - } -} - - -class NotebookChatEditorController extends Disposable { - private readonly deletedCellDecorator: NotebookDeletedCellDecorator; - private readonly insertedCellDecorator: NotebookInsertedCellDecorator; - private readonly _ctxHasEditorModification: IContextKey; - constructor( - private readonly notebookEditor: INotebookEditor, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @INotebookOriginalModelReferenceFactory private readonly originalModelRefFactory: INotebookOriginalModelReferenceFactory, - @INotebookModelSynchronizerFactory private readonly synchronizerFactory: INotebookModelSynchronizerFactory, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - ) { - super(); - this._ctxHasEditorModification = ctxNotebookHasEditorModification.bindTo(contextKeyService); - this.deletedCellDecorator = this._register(instantiationService.createInstance(NotebookDeletedCellDecorator, notebookEditor, undefined)); - this.insertedCellDecorator = this._register(instantiationService.createInstance(NotebookInsertedCellDecorator, notebookEditor)); - const notebookModel = observableFromEvent(this.notebookEditor.onDidChangeModel, e => e); - const originalModel = observableValue('originalModel', undefined); - // We need to render viewzones only when the viewmodel is attached (i.e. list view is ready). - // https://github.com/microsoft/vscode/issues/234718 - const readyToRenderViewzones = observableValue('viewModelAttached', false); - this._register(Event.once(this.notebookEditor.onDidAttachViewModel)(() => readyToRenderViewzones.set(true, undefined))); - const onDidChangeVisibleRanges = debouncedObservable(observableFromEvent(this.notebookEditor.onDidChangeVisibleRanges, () => this.notebookEditor.visibleRanges), 50); - const decorators = new Map(); - - let updatedCellDecoratorsOnceBefore = false; - let updatedDeletedInsertedDecoratorsOnceBefore = false; - - - const clearDecorators = () => { - dispose(Array.from(decorators.values())); - decorators.clear(); - this.deletedCellDecorator.clear(); - this.insertedCellDecorator.clear(); - }; - - this._register(toDisposable(() => clearDecorators())); - - let notebookSynchronizer: IReference; - const entryObs = derived((r) => { - const model = notebookModel.read(r); - if (!model) { - return; - } - const sessions = this._chatEditingService.editingSessionsObs.read(r); - const entry = sessions.map(s => s.readEntry(model.uri, r)).find(r => !!r); - return entry instanceof ChatEditingModifiedDocumentEntry ? entry : undefined; - }).recomputeInitiallyAndOnChange(this._store); - - - this._register(autorun(r => { - const entry = entryObs.read(r); - const model = notebookModel.read(r); - if (!entry || !model || entry.state.read(r) !== WorkingSetEntryState.Modified) { - clearDecorators(); - } - })); - - const notebookDiffInfo = derivedWithStore(this, (r, store) => { - const entry = entryObs.read(r); - const model = notebookModel.read(r); - if (!entry || !model) { - // If entry is undefined, then revert the changes to the notebook. - if (notebookSynchronizer && model) { - notebookSynchronizer.object.revert(); - } - return observableValue<{ - cellDiff: CellDiffInfo[]; - modelVersion: number; - } | undefined>('DefaultDiffIno', undefined); - } - - notebookSynchronizer = notebookSynchronizer || this._register(this.synchronizerFactory.getOrCreate(model)); - this.originalModelRefFactory.getOrCreate(entry, model.viewType).then(ref => originalModel.set(this._register(ref).object, undefined)); - - return notebookSynchronizer.object.diffInfo; - }).recomputeInitiallyAndOnChange(this._store).flatten(); - - const notebookCellDiffInfo = notebookDiffInfo.map(d => d?.cellDiff); - this._register(instantiationService.createInstance(NotebookChatActionsOverlayController, notebookEditor, notebookCellDiffInfo, this.deletedCellDecorator)); - - this._register(autorun(r => { - // If we have a new entry for the file, then clear old decorators. - // User could be cycling through different edit sessions (Undo Last Edit / Redo Last Edit). - entryObs.read(r); - clearDecorators(); - })); - - this._register(autorun(r => { - // If there's no diff info, then we either accepted or rejected everything. - const diffs = notebookDiffInfo.read(r); - if (!diffs || !diffs.cellDiff.length) { - clearDecorators(); - this._ctxHasEditorModification.reset(); - } else { - this._ctxHasEditorModification.set(true); - } - })); - - this._register(autorun(r => { - const entry = entryObs.read(r); - const diffInfo = notebookDiffInfo.read(r); - const modified = notebookModel.read(r); - const original = originalModel.read(r); - onDidChangeVisibleRanges.read(r); - - if (!entry || !modified || !original || !diffInfo) { - return; - } - if (diffInfo && updatedCellDecoratorsOnceBefore && (diffInfo.modelVersion !== modified.versionId)) { - return; - } - - updatedCellDecoratorsOnceBefore = true; - const validDiffDecorators = new Set(); - diffInfo.cellDiff.forEach((diff) => { - if (diff.type === 'modified') { - const modifiedCell = modified.cells[diff.modifiedCellIndex]; - const originalCell = original.cells[diff.originalCellIndex]; - const editor = this.notebookEditor.codeEditors.find(([vm,]) => vm.handle === modifiedCell.handle)?.[1]; - - if (editor) { - const currentDecorator = decorators.get(modifiedCell); - if ((currentDecorator?.modifiedCell !== modifiedCell || currentDecorator?.originalCell !== originalCell)) { - currentDecorator?.dispose(); - const decorator = this.instantiationService.createInstance(NotebookCellDiffDecorator, notebookEditor, modifiedCell, originalCell); - decorators.set(modifiedCell, decorator); - validDiffDecorators.add(decorator); - this._register(editor.onDidDispose(() => { - decorator.dispose(); - if (decorators.get(modifiedCell) === decorator) { - decorators.delete(modifiedCell); - } - })); - } else if (currentDecorator) { - validDiffDecorators.add(currentDecorator); - } - } - } - }); - - // Dispose old decorators - decorators.forEach((v, cell) => { - if (!validDiffDecorators.has(v)) { - v.dispose(); - decorators.delete(cell); - } - }); - })); - - this._register(autorun(r => { - const entry = entryObs.read(r); - const diffInfo = notebookDiffInfo.read(r); - const modified = notebookModel.read(r); - const original = originalModel.read(r); - const ready = readyToRenderViewzones.read(r); - if (!ready || !entry || !modified || !original || !diffInfo) { - return; - } - if (diffInfo && updatedDeletedInsertedDecoratorsOnceBefore && (diffInfo.modelVersion !== modified.versionId)) { - return; - } - updatedDeletedInsertedDecoratorsOnceBefore = true; - this.insertedCellDecorator.apply(diffInfo.cellDiff); - this.deletedCellDecorator.apply(diffInfo.cellDiff, original); - })); - } - -} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts deleted file mode 100644 index dbebccbe2f9..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts +++ /dev/null @@ -1,500 +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 { isEqual } from '../../../../../../base/common/resources.js'; -import { Disposable, IReference, ReferenceCollection } from '../../../../../../base/common/lifecycle.js'; -import { IChatEditingService, IModifiedFileEntry } from '../../../../chat/common/chatEditingService.js'; -import { INotebookService } from '../../../common/notebookService.js'; -import { bufferToStream, streamToBuffer, VSBuffer } from '../../../../../../base/common/buffer.js'; -import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; -import { raceCancellation, ThrottledDelayer } from '../../../../../../base/common/async.js'; -import { CellDiffInfo, computeDiff, prettyChanges } from '../../diff/notebookDiffViewModel.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; -import { INotebookEditorWorkerService } from '../../../common/services/notebookWorkerService.js'; -import { CellEditType, ICellDto2, ICellEditOperation, ICellReplaceEdit, NotebookData, NotebookSetting } from '../../../common/notebookCommon.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { EditOperation } from '../../../../../../editor/common/core/editOperation.js'; -import { INotebookLoggingService } from '../../../common/notebookLoggingService.js'; -import { filter } from '../../../../../../base/common/objects.js'; -import { INotebookEditorModelResolverService } from '../../../common/notebookEditorModelResolverService.js'; -import { IChatService } from '../../../../chat/common/chatService.js'; -import { createDecorator, IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { INotebookOriginalModelReferenceFactory } from './notebookOriginalModelRefFactory.js'; -import { autorunWithStore, derived, IObservable, observableValue } from '../../../../../../base/common/observable.js'; -import { SaveReason } from '../../../../../common/editor.js'; -import { ITextModelService } from '../../../../../../editor/common/services/resolverService.js'; -import { SnapshotContext } from '../../../../../services/workingCopy/common/fileWorkingCopy.js'; -import { INotebookEditorService } from '../../services/notebookEditorService.js'; -import { CellEditState } from '../../notebookBrowser.js'; -import { IModelService } from '../../../../../../editor/common/services/model.js'; -import { ChatEditingModifiedDocumentEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedDocumentEntry.js'; - - -export const INotebookModelSynchronizerFactory = createDecorator('INotebookModelSynchronizerFactory'); - -export interface INotebookModelSynchronizerFactory { - readonly _serviceBrand: undefined; - getOrCreate(model: NotebookTextModel): IReference; -} - -class NotebookModelSynchronizerReferenceCollection extends ReferenceCollection { - constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { - super(); - } - protected override createReferencedObject(_key: string, model: NotebookTextModel): NotebookModelSynchronizer { - return this.instantiationService.createInstance(NotebookModelSynchronizer, model); - } - protected override destroyReferencedObject(_key: string, object: NotebookModelSynchronizer): void { - object.dispose(); - } -} - -export class NotebookModelSynchronizerFactory implements INotebookModelSynchronizerFactory { - readonly _serviceBrand: undefined; - private readonly _data: NotebookModelSynchronizerReferenceCollection; - constructor(@IInstantiationService instantiationService: IInstantiationService) { - this._data = instantiationService.createInstance(NotebookModelSynchronizerReferenceCollection); - } - - getOrCreate(model: NotebookTextModel): IReference { - return this._data.acquire(model.uri.toString(), model); - } -} - - -export class NotebookModelSynchronizer extends Disposable { - private readonly throttledUpdateNotebookModel = new ThrottledDelayer(200); - private _diffInfo = observableValue<{ cellDiff: CellDiffInfo[]; modelVersion: number } | undefined>('diffInfo', undefined); - public get diffInfo(): IObservable<{ cellDiff: CellDiffInfo[]; modelVersion: number } | undefined> { - return this._diffInfo; - } - private snapshot?: { bytes: VSBuffer; dirty: boolean }; - private isEditFromUs: boolean = false; - private isTextEditFromUs: boolean = false; - private isReverting = false; - private throttledTextModelUpdate = new ThrottledDelayer(100); - constructor( - private readonly model: NotebookTextModel, - @IChatEditingService _chatEditingService: IChatEditingService, - @INotebookService private readonly notebookService: INotebookService, - @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, - @IChatService chatService: IChatService, - @IModelService private readonly modelService: IModelService, - @ITextModelService private readonly textModelService: ITextModelService, - @INotebookLoggingService private readonly logService: INotebookLoggingService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @INotebookEditorWorkerService private readonly notebookEditorWorkerService: INotebookEditorWorkerService, - @INotebookEditorModelResolverService private readonly notebookModelResolverService: INotebookEditorModelResolverService, - @INotebookOriginalModelReferenceFactory private readonly originalModelRefFactory: INotebookOriginalModelReferenceFactory, - ) { - super(); - - const entryObs = derived((r) => { - const sessions = _chatEditingService.editingSessionsObs.read(r); - const entry = sessions.map(s => s.readEntry(model.uri, r)).find(r => !!r); - return entry instanceof ChatEditingModifiedDocumentEntry ? entry : undefined; - }).recomputeInitiallyAndOnChange(this._store); - - - this._register(chatService.onDidPerformUserAction(async e => { - const entry = entryObs.read(undefined); - if (!entry) { - return; - } - if (e.action.kind === 'chatEditingSessionAction' && !e.action.hasRemainingEdits && isEqual(e.action.uri, entry.modifiedURI)) { - if (e.action.outcome === 'accepted') { - await this.accept(entry); - await this.createSnapshot(); - this._diffInfo.set(undefined, undefined); - } - else if (e.action.outcome === 'rejected') { - await this.revertImpl(); - } - } - })); - - const updateNotebookModel = (entry: IModifiedFileEntry, token: CancellationToken) => { - this.throttledUpdateNotebookModel.trigger(() => this.updateNotebookModel(entry, token)); - }; - - let snapshotCreated = false; - this._register(autorunWithStore((r, store) => { - const entry = entryObs.read(r); - if (!entry) { - return; - } - if (!snapshotCreated) { - this.createSnapshot(); - snapshotCreated = true; - } - - const modifiedModel = this.modelService.getModel(entry.modifiedURI); - if (!modifiedModel) { - return; - } - let cancellationToken = store.add(new CancellationTokenSource()); - store.add(modifiedModel.onDidChangeContent(async () => { - const originalModel = this.modelService.getModel(entry.originalURI); - if (originalModel && !this.isTextEditFromUs && !modifiedModel.isDisposed() && !originalModel.isDisposed() && modifiedModel.getValue() !== originalModel.getValue()) { - cancellationToken = store.add(new CancellationTokenSource()); - updateNotebookModel(entry, cancellationToken.token); - } - })); - - updateNotebookModel(entry, cancellationToken.token); - })); - - this._register(model.onDidChangeContent(() => { - // Track changes from the user. - if (!this.isEditFromUs && this.snapshot) { - this.snapshot.dirty = true; - const entry = entryObs.get(); - if (entry) { - this.throttledTextModelUpdate.trigger(() => this.updateTextDocumentModel(entry)); - } - } - })); - } - - private async createSnapshot() { - const [serializer, ref] = await Promise.all([ - this.getNotebookSerializer(), - this.notebookModelResolverService.resolve(this.model.uri) - ]); - - try { - const data: NotebookData = { - metadata: filter(this.model.metadata, key => !serializer.options.transientDocumentMetadata[key]), - cells: [], - }; - - const indentAmount = this.model.metadata.indentAmount || ref.object.notebook.metadata.indentAmount || undefined; - if (typeof indentAmount === 'string' && indentAmount) { - // This is required for ipynb serializer to preserve the whitespace in the notebook. - data.metadata.indentAmount = indentAmount; - } - - let outputSize = 0; - for (const cell of this.model.cells) { - const cellData: ICellDto2 = { - cellKind: cell.cellKind, - language: cell.language, - mime: cell.mime, - source: cell.getValue(), - outputs: [], - internalMetadata: cell.internalMetadata - }; - - const outputSizeLimit = this.configurationService.getValue(NotebookSetting.outputBackupSizeLimit) * 1024; - if (outputSizeLimit > 0) { - cell.outputs.forEach(output => { - output.outputs.forEach(item => { - outputSize += item.data.byteLength; - }); - }); - if (outputSize > outputSizeLimit) { - return; - } - } - - cellData.outputs = !serializer.options.transientOutputs ? cell.outputs : []; - cellData.metadata = filter(cell.metadata, key => !serializer.options.transientCellMetadata[key]); - - data.cells.push(cellData); - } - - const bytes = await serializer.notebookToData(data); - this.snapshot = { bytes, dirty: ref.object.isDirty() }; - } finally { - ref.dispose(); - } - } - - public async revert() { - await this.revertImpl(); - } - - private async revertImpl(): Promise { - if (!this.snapshot || this.isReverting) { - return; - } - this.isReverting = true; - try { - // NOTE: We must save if the notebook model was not already dirty. - // Today ModifiedFileEntry class will save the text model to get rid of dirty indicator. - // If we do not save the notebook model, then ipynb json text document will get saved in ModifiedFileEntry class, - // and that results in ipynb being saved without going to serializer. - // Serializer is responsible for adding new line to ipynb files, and that new line will not be added when saving ipynb text document. - // As a result of this, reverting (creating new edit sessions), result in ipynb files without new line at the end meaning we still end up with a saved ipynb file with changes. - // Hence we must ensure ipynb notebooks are always saved through serializer. - // But do this only if the notebook model was not already dirty. - await this.updateNotebook(this.snapshot.bytes, !this.snapshot.dirty); - } - finally { - this.isReverting = false; - this._diffInfo.set(undefined, undefined); - } - } - - private async updateNotebook(bytes: VSBuffer, saveForRevert: boolean) { - const oldEditIsFromus = this.isEditFromUs; - this.isEditFromUs = true; - const ref = await this.notebookModelResolverService.resolve(this.model.uri); - try { - const serializer = await this.getNotebookSerializer(); - const data = await serializer.dataToNotebook(bytes); - this.model.reset(data.cells, data.metadata, serializer.options); - if (saveForRevert) { - // When reverting/creating a new session ModifiedFileEntry will revert and save changes to ipynb text document first, and save the file. - // This happens in the NotebookSyncrhonizerService which is called from SimpleNotebookEditorModel (NotebookEditorModel.ts). - // However when creating new sessions, the modified File entry will not exist as its a whole new session, - // As a result we aren't able to save the ipynb text document and match the last modified date time. - // Hence the work around implemented in SimpleNotebookEditorModel does not work. - // Thus we must save the file here igorning the modified since time, but only when reverting. - await ref.object.save({ reason: SaveReason.EXPLICIT, force: true, ignoreModifiedSince: true } as any); - } - } finally { - ref.dispose(); - this.isEditFromUs = oldEditIsFromus; - } - } - - private async accept(entry: IModifiedFileEntry) { - const modifiedModel = this.modelService.getModel(entry.modifiedURI); - if (!modifiedModel) { - return; - } - const content = modifiedModel.getValue(); - await this.updateNotebook(VSBuffer.fromString(content), false); - this._diffInfo.set(undefined, undefined); - - // The original notebook model needs to be updated with the latest content. - const stream = await this.notebookService.createNotebookTextDocumentSnapshot(this.model.uri, SnapshotContext.Save, CancellationToken.None); - const originalModel = await this.getOriginalModel(entry); - await this.notebookService.restoreNotebookTextModelFromSnapshot(originalModel.uri, originalModel.viewType, stream); - } - - private async updateTextDocumentModel(entry: IModifiedFileEntry) { - const modifiedModel = this.modelService.getModel(entry.modifiedURI); - if (!modifiedModel) { - return; - } - const stream = await this.notebookService.createNotebookTextDocumentSnapshot(this.model.uri, SnapshotContext.Save, CancellationToken.None); - const buffer = await streamToBuffer(stream); - const text = new TextDecoder().decode(buffer.buffer); - this.isTextEditFromUs = true; - modifiedModel.pushEditOperations(null, [{ range: modifiedModel.getFullModelRange(), text }], () => null); - this.isTextEditFromUs = false; - } - - async getNotebookSerializer() { - const info = await this.notebookService.withNotebookDataProvider(this.model.viewType); - return info.serializer; - } - - private _originalModel?: Promise; - private async getOriginalModel(entry: IModifiedFileEntry): Promise { - if (!this._originalModel) { - this._originalModel = this.originalModelRefFactory.getOrCreate(entry, this.model.viewType).then(ref => { - if (this._store.isDisposed) { - ref.dispose(); - return ref.object; - } else { - return this._register(ref).object; - } - }); - } - return this._originalModel; - } - - private async updateNotebookModel(entry: IModifiedFileEntry, token: CancellationToken) { - const modifiedModel = this.modelService.getModel(entry.modifiedURI); - if (!modifiedModel) { - return; - } - - const modifiedModelVersion = modifiedModel.getVersionId(); - const currentModel = this.model; - const modelVersion = currentModel?.versionId ?? 0; - const modelWithChatEdits = await this.getModifiedModelForDiff(entry, token); - if (!modelWithChatEdits || token.isCancellationRequested || !currentModel) { - return; - } - const originalModel = await this.getOriginalModel(entry); - // This is the total diff from the original model to the model with chat edits. - const cellDiffInfo = (await this.computeDiff(originalModel, modelWithChatEdits, token))?.cellDiffInfo; - // This is the diff from the current model to the model with chat edits. - const cellDiffInfoToApplyEdits = (await this.computeDiff(currentModel, modelWithChatEdits, token))?.cellDiffInfo; - const currentVersion = modifiedModel.getVersionId(); - if (!cellDiffInfo || !cellDiffInfoToApplyEdits || token.isCancellationRequested || currentVersion !== modifiedModelVersion || modelVersion !== currentModel.versionId) { - return; - } - if (cellDiffInfoToApplyEdits.every(d => d.type === 'unchanged')) { - return; - } - - // All edits from here on are from us. - this.isEditFromUs = true; - try { - const edits: ICellReplaceEdit[] = []; - const mappings = new Map(); - - // First Delete. - const deletedIndexes: number[] = []; - await Promise.all(cellDiffInfoToApplyEdits.reverse().map(async diff => { - if (diff.type === 'delete') { - deletedIndexes.push(diff.originalCellIndex); - edits.push({ - editType: CellEditType.Replace, - index: diff.originalCellIndex, - cells: [], - count: 1 - }); - } - })); - if (edits.length) { - currentModel.applyEdits(edits, true, undefined, () => undefined, undefined, false); - edits.length = 0; - } - - const notebookEditor = this.notebookEditorService.retrieveExistingWidgetFromURI(this.model.uri)?.value; - - // Next insert. - cellDiffInfoToApplyEdits.reverse().forEach(diff => { - if (diff.type === 'modified' || diff.type === 'unchanged') { - mappings.set(diff.modifiedCellIndex, diff.originalCellIndex); - } - if (diff.type === 'insert') { - const originalIndex = mappings.get(diff.modifiedCellIndex - 1) ?? 0; - mappings.set(diff.modifiedCellIndex, originalIndex); - const index = currentModel.cells.length ? originalIndex + 1 : originalIndex; - const cell = modelWithChatEdits.cells[diff.modifiedCellIndex]; - const newCell: ICellDto2 = - { - source: cell.getValue(), - cellKind: cell.cellKind, - language: cell.language, - outputs: cell.outputs.map(output => output.asDto()), - mime: cell.mime, - metadata: cell.metadata, - collapseState: cell.collapseState, - internalMetadata: cell.internalMetadata - }; - edits.push({ - editType: CellEditType.Replace, - index, - cells: [newCell], - count: 0 - }); - } - }); - if (edits.length) { - currentModel.applyEdits(edits, true, undefined, () => undefined, undefined, false); - for (const edit of edits.filter(e => e.editType === CellEditType.Replace)) { - const cell = currentModel.cells[edit.index]; - if (cell) { - const cellViewModel = notebookEditor?.getCellByHandle(cell.handle); - cellViewModel?.updateEditState(CellEditState.Editing, 'chatEdit'); - } - } - edits.length = 0; - } - - // Finally update - await Promise.all(cellDiffInfoToApplyEdits.map(async diff => { - if (diff.type === 'modified') { - const cell = currentModel.cells[diff.originalCellIndex]; - // Ensure the models of these cells have been loaded before we update them. - const cellModelRef = await this.textModelService.createModelReference(cell.uri); - try { - const modifiedCell = modelWithChatEdits.cells[diff.modifiedCellIndex]; - if (modifiedCell.cellKind === cell.cellKind) { - const cellViewModel = notebookEditor?.getCellByHandle(cell.handle); - cellViewModel?.updateEditState(CellEditState.Editing, 'chatEdit'); - const textModel = cellModelRef.object.textEditorModel; - textModel.pushEditOperations(null, [ - EditOperation.replace(textModel.getFullModelRange(), modifiedCell.getValue()) - ], () => null); - } else { - const newCellDto: ICellDto2 = - { - source: modifiedCell.getValue(), - cellKind: modifiedCell.cellKind, - language: modifiedCell.language, - outputs: modifiedCell.outputs.map(output => output.asDto()), - mime: modifiedCell.mime, - metadata: modifiedCell.metadata, - collapseState: modifiedCell.collapseState, - internalMetadata: modifiedCell.internalMetadata - }; - const edit: ICellEditOperation = { - editType: CellEditType.Replace, - index: diff.originalCellIndex, - cells: [newCellDto], - count: 1 - }; - currentModel.applyEdits([edit], true, undefined, () => undefined, undefined, false); - const newCell = currentModel.cells[diff.originalCellIndex]; - const cellViewModel = notebookEditor?.getCellByHandle(newCell.handle); - cellViewModel?.updateEditState(CellEditState.Editing, 'chatEdit'); - } - } finally { - cellModelRef.dispose(); - } - } - })); - - if (edits.length) { - currentModel.applyEdits(edits, true, undefined, () => undefined, undefined, false); - } - this._diffInfo.set({ cellDiff: cellDiffInfo, modelVersion: currentModel.versionId }, undefined); - } - finally { - this.isEditFromUs = false; - } - } - private previousUriOfModelForDiff?: URI; - - private async getModifiedModelForDiff(entry: IModifiedFileEntry, token: CancellationToken): Promise { - const modifiedModel = this.modelService.getModel(entry.modifiedURI); - if (!modifiedModel) { - return; - } - const text = modifiedModel.getValue(); - const bytes = VSBuffer.fromString(text); - const uri = entry.modifiedURI.with({ scheme: `NotebookChatEditorController.modifiedScheme${Date.now().toString()}` }); - const stream = bufferToStream(bytes); - if (this.previousUriOfModelForDiff) { - this.notebookService.getNotebookTextModel(this.previousUriOfModelForDiff)?.dispose(); - } - this.previousUriOfModelForDiff = uri; - try { - const model = await this.notebookService.createNotebookTextModel(this.model.viewType, uri, stream); - if (token.isCancellationRequested) { - model.dispose(); - return; - } - this._register(model); - return model; - } catch (ex) { - this.logService.warn('NotebookChatEdit', `Failed to deserialize Notebook for ${uri.toString}, ${ex.message}`); - this.logService.debug('NotebookChatEdit', ex.toString()); - return; - } - } - - async computeDiff(original: NotebookTextModel, modified: NotebookTextModel, token: CancellationToken) { - const diffResult = await raceCancellation(this.notebookEditorWorkerService.computeDiff(original.uri, modified.uri), token); - if (!diffResult || token.isCancellationRequested) { - // after await the editor might be disposed. - return; - } - - prettyChanges(original, modified, diffResult.cellsDiff); - - return computeDiff(original, modified, diffResult); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts deleted file mode 100644 index 5d788c4c0b4..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts +++ /dev/null @@ -1,94 +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 { IFileService } from '../../../../../../platform/files/common/files.js'; -import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, StoredFileWorkingCopy } from '../../../../../services/workingCopy/common/storedFileWorkingCopy.js'; -import { IUntitledFileWorkingCopy } from '../../../../../services/workingCopy/common/untitledFileWorkingCopy.js'; -import { IStoredFileWorkingCopySaveParticipantContext, IWorkingCopyFileService } from '../../../../../services/workingCopy/common/workingCopyFileService.js'; -import { IChatEditingService } from '../../../../chat/common/chatEditingService.js'; -import { NotebookFileWorkingCopyModel } from '../../../common/notebookEditorModel.js'; -import { INotebookSynchronizerService } from '../../../common/notebookSynchronizerService.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { NotebookSaveParticipant } from '../saveParticipants/saveParticipants.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { IProgress, IProgressStep } from '../../../../../../platform/progress/common/progress.js'; -import { IEditorService } from '../../../../../services/editor/common/editorService.js'; -import { INotebookService } from '../../../common/notebookService.js'; -import { ChatEditingModifiedDocumentEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedDocumentEntry.js'; -import { SaveReason } from '../../../../../common/editor.js'; - -class NotebookSynchronizerSaveParticipant extends NotebookSaveParticipant { - constructor( - @IEditorService editorService: IEditorService, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IFileService protected readonly _fileService: IFileService, - @INotebookService private readonly _notebookService: INotebookService, - ) { - super(editorService); - } - - override async participate(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { - const sessions = this._chatEditingService.editingSessionsObs.get(); - const session = sessions.find(s => s.getEntry(workingCopy.resource)); - if (!session) { - return; - } - - if (!this._notebookService.hasSupportedNotebooks(workingCopy.resource)) { - return; - } - - const entry = session.getEntry(workingCopy.resource); - - if (entry && entry instanceof ChatEditingModifiedDocumentEntry) { - await entry.docFileEditorModel.save({ reason: SaveReason.EXPLICIT, ignoreModifiedSince: true }); - } - - const inWorkingSet = session.workingSet.has(workingCopy.resource); - - if (!(entry && entry instanceof ChatEditingModifiedDocumentEntry) && !inWorkingSet) { - // file not in working set, no need to continue - return; - } - - if (workingCopy instanceof StoredFileWorkingCopy) { - const metadata = await this._fileService.stat(workingCopy.resource); - if (workingCopy.lastResolvedFileStat) { - workingCopy.lastResolvedFileStat = { - ...workingCopy.lastResolvedFileStat, - ...metadata - }; - } - } - } -} - -export class NotebookSynchronizerService extends Disposable implements INotebookSynchronizerService { - _serviceBrand: undefined; - - constructor( - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IFileService protected readonly _fileService: IFileService, - @INotebookService private readonly _notebookService: INotebookService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService) { - super(); - this._register(this._workingCopyFileService.addSaveParticipant(this._instantiationService.createInstance(NotebookSynchronizerSaveParticipant))); - } - - async revert(workingCopy: IStoredFileWorkingCopy | IUntitledFileWorkingCopy) { - // check if we have mirror document - const resource = workingCopy.resource; - if (!this._notebookService.hasSupportedNotebooks(workingCopy.resource)) { - return; - } - const sessions = this._chatEditingService.editingSessionsObs.get(); - const entry = sessions.map(s => s.getEntry(resource)).find(r => !!r); - if (entry instanceof ChatEditingModifiedDocumentEntry) { - await entry.docFileEditorModel.revert({ soft: true }); - } - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts index 8ce231cbd24..116d5d3fab9 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts @@ -1035,7 +1035,7 @@ class NotebookAddMatchToMultiSelectionAction extends NotebookAction { constructor() { super({ id: NOTEBOOK_ADD_FIND_MATCH_TO_SELECTION_ID, - title: localize('addFindMatchToSelection', "Add Selection To Next Find Match"), + title: localize('addFindMatchToSelection', "Add Selection to Next Find Match"), precondition: ContextKeyExpr.and( ContextKeyExpr.equals('config.notebook.multiCursor.enabled', true), NOTEBOOK_IS_ACTIVE_EDITOR, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts index 38164a2b381..57103835244 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts @@ -12,6 +12,7 @@ import { isEqual } from '../../../../../../base/common/resources.js'; import { format } from '../../../../../../base/common/strings.js'; import { Position } from '../../../../../../editor/common/core/position.js'; import { Range } from '../../../../../../editor/common/core/range.js'; +import { StandardTokenType } from '../../../../../../editor/common/encodedTokenAttributes.js'; import { InlineValueContext, InlineValueText, InlineValueVariableLookup } from '../../../../../../editor/common/languages.js'; import { IModelDeltaDecoration, ITextModel } from '../../../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; @@ -42,6 +43,8 @@ export class NotebookInlineVariablesController extends Disposable implements INo private currentCancellationTokenSources = new ResourceMap(); + private static readonly MAX_CELL_LINES = 5000; // Skip extremely large cells + constructor( private readonly notebookEditor: INotebookEditor, @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, @@ -236,10 +239,18 @@ export class NotebookInlineVariablesController extends Disposable implements INo return; } + // Skip processing for extremely large cells + if (document.getLineCount() > NotebookInlineVariablesController.MAX_CELL_LINES) { + return; + } + const inlineDecorations: IModelDeltaDecoration[] = []; const processedVars = new Set(); + // Get both function ranges and comment ranges const functionRanges = this.getFunctionRanges(document); + const commentedRanges = this.getCommentedRanges(document); + const ignoredRanges = [...functionRanges, ...commentedRanges]; const lineDecorations = new Map(); // For each variable name found in the kernel results @@ -248,46 +259,47 @@ export class NotebookInlineVariablesController extends Disposable implements INo continue; } - // Look for variable usage globally - const regex = new RegExp(`\\b${varName}\\b`, 'g'); - let lastMatchOutsideFunction: { line: number; column: number } | null = null; + // Look for variable usage globally - using word boundaries to ensure exact matches + const regex = new RegExp(`\\b${varName}\\b(?!\\w)`, 'g'); + let lastMatchOutsideIgnored: { line: number; column: number } | null = null; + let foundMatch = false; + // Scan lines in reverse to find last occurrence first const lines = document.getValue().split('\n'); - for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) { + for (let lineNumber = lines.length - 1; lineNumber >= 0; lineNumber--) { const line = lines[lineNumber]; let match: RegExpExecArray | null; while ((match = regex.exec(line)) !== null) { - const pos = new Position(lineNumber + 1, match.index + 1); - let isInFunction = false; + const startIndex = match.index; + const pos = new Position(lineNumber + 1, startIndex + 1); - // Check if this usage is within any function range - for (const range of functionRanges) { - if (range.containsPosition(pos)) { - isInFunction = true; - break; - } - } - - if (!isInFunction) { - lastMatchOutsideFunction = { + // Check if this position is in any ignored range (function or comment) + if (!this.isPositionInRanges(pos, ignoredRanges)) { + lastMatchOutsideIgnored = { line: lineNumber + 1, - column: match.index + 1 + column: startIndex + 1 }; + foundMatch = true; + break; // Take first match in reverse order (which is last chronologically) } } + + if (foundMatch) { + break; // We found our last valid occurrence, no need to check earlier lines + } } - if (lastMatchOutsideFunction) { + if (lastMatchOutsideIgnored) { const inlineVal = varName + ' = ' + vars.find(v => v.name === varName)?.value; - let lineSegments = lineDecorations.get(lastMatchOutsideFunction.line); + let lineSegments = lineDecorations.get(lastMatchOutsideIgnored.line); if (!lineSegments) { lineSegments = []; - lineDecorations.set(lastMatchOutsideFunction.line, lineSegments); + lineDecorations.set(lastMatchOutsideIgnored.line, lineSegments); } if (!lineSegments.some(iv => iv.text === inlineVal)) { // de-dupe - lineSegments.push(new InlineSegment(lastMatchOutsideFunction.column, inlineVal)); + lineSegments.push(new InlineSegment(lastMatchOutsideIgnored.column, inlineVal)); } } @@ -414,6 +426,157 @@ export class NotebookInlineVariablesController extends Disposable implements INo return functionRanges; } + private getCommentedRanges(document: ITextModel): Range[] { + return this._getCommentedRanges(document); + } + + private _getCommentedRanges(document: ITextModel): Range[] { + try { + return this.getCommentedRangesByAccurateTokenization(document); + } catch (e) { + // Fall back to manual parsing if tokenization fails + return this.getCommentedRangesByManualParsing(document); + } + } + + private getCommentedRangesByAccurateTokenization(document: ITextModel): Range[] { + const commentRanges: Range[] = []; + const lineCount = document.getLineCount(); + + // Skip processing for extremely large documents + if (lineCount > NotebookInlineVariablesController.MAX_CELL_LINES) { + return commentRanges; + } + + // Process each line - force tokenization if needed and process tokens in a single pass + for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) { + // Force tokenization if needed + if (!document.tokenization.hasAccurateTokensForLine(lineNumber)) { + document.tokenization.forceTokenization(lineNumber); + } + + const lineTokens = document.tokenization.getLineTokens(lineNumber); + + // Skip lines with no tokens + if (lineTokens.getCount() === 0) { + continue; + } + + let startCharacter: number | undefined; + + // Check each token in the line + for (let tokenIndex = 0; tokenIndex < lineTokens.getCount(); tokenIndex++) { + const tokenType = lineTokens.getStandardTokenType(tokenIndex); + + if (tokenType === StandardTokenType.Comment || tokenType === StandardTokenType.String || tokenType === StandardTokenType.RegEx) { + if (startCharacter === undefined) { + // Start of a comment or string + startCharacter = lineTokens.getStartOffset(tokenIndex); + } + + const endCharacter = lineTokens.getEndOffset(tokenIndex); + + // Check if this is the end of the comment/string section (either end of line or different token type follows) + const isLastToken = tokenIndex === lineTokens.getCount() - 1; + const nextTokenDifferent = !isLastToken && + lineTokens.getStandardTokenType(tokenIndex + 1) !== tokenType; + + if (isLastToken || nextTokenDifferent) { + // End of comment/string section + commentRanges.push(new Range(lineNumber, startCharacter + 1, lineNumber, endCharacter + 1)); + startCharacter = undefined; + } + } else { + // Reset when we hit a non-comment, non-string token + startCharacter = undefined; + } + } + } + + return commentRanges; + } + + private getCommentedRangesByManualParsing(document: ITextModel): Range[] { + const commentRanges: Range[] = []; + const lines = document.getValue().split('\n'); + const languageId = document.getLanguageId(); + + // Different comment patterns by language + const lineCommentToken = + languageId === 'python' ? '#' : + languageId === 'javascript' || languageId === 'typescript' ? '//' : + null; + + const blockComments = + (languageId === 'javascript' || languageId === 'typescript') ? { start: '/*', end: '*/' } : + null; + + let inBlockComment = false; + let blockCommentStartLine = -1; + let blockCommentStartCol = -1; + + for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) { + const line = lines[lineNumber]; + const trimmedLine = line.trim(); + + // Skip empty lines + if (trimmedLine.length === 0) { + continue; + } + + if (blockComments) { + if (!inBlockComment) { + const startIndex = line.indexOf(blockComments.start); + if (startIndex !== -1) { + inBlockComment = true; + blockCommentStartLine = lineNumber; + blockCommentStartCol = startIndex; + } + } + + if (inBlockComment) { + const endIndex = line.indexOf(blockComments.end); + if (endIndex !== -1) { + commentRanges.push(new Range( + blockCommentStartLine + 1, + blockCommentStartCol + 1, + lineNumber + 1, + endIndex + blockComments.end.length + 1 + )); + inBlockComment = false; + } + continue; + } + } + + if (!inBlockComment && lineCommentToken && line.trimLeft().startsWith(lineCommentToken)) { + const startCol = line.indexOf(lineCommentToken); + commentRanges.push(new Range( + lineNumber + 1, + startCol + 1, + lineNumber + 1, + line.length + 1 + )); + } + } + + // Handle block comment at end of file + if (inBlockComment) { + commentRanges.push(new Range( + blockCommentStartLine + 1, + blockCommentStartCol + 1, + lines.length, + lines[lines.length - 1].length + 1 + )); + } + + return commentRanges; + } + + private isPositionInRanges(position: Position, ranges: Range[]): boolean { + return ranges.some(range => range.containsPosition(position)); + } + private updateCellInlineDecorations(cell: ICellViewModel, decorations: IModelDeltaDecoration[]) { const oldDecorations = this.cellDecorationIds.get(cell) ?? []; this.cellDecorationIds.set(cell, cell.deltaModelDecorations( @@ -428,6 +591,7 @@ export class NotebookInlineVariablesController extends Disposable implements INo return; // should not happen } + // Clear decorations on content change this.cellContentListeners.set(cell.uri, cellModel.onDidChangeContent(() => { this.clearCellInlineDecorations(cell); })); @@ -462,6 +626,8 @@ export class NotebookInlineVariablesController extends Disposable implements INo this._clearNotebookInlineDecorations(); this.currentCancellationTokenSources.forEach(source => source.cancel()); this.currentCancellationTokenSources.clear(); + this.cellContentListeners.forEach(listener => listener.dispose()); + this.cellContentListeners.clear(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts index fd5ac883a4c..c2f474c1039 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts @@ -23,7 +23,9 @@ import { IChatWidget, IChatWidgetService } from '../../../../chat/browser/chat.j import { ChatInputPart } from '../../../../chat/browser/chatInputPart.js'; import { ChatDynamicVariableModel } from '../../../../chat/browser/contrib/chatDynamicVariables.js'; import { computeCompletionRanges } from '../../../../chat/browser/contrib/chatInputCompletions.js'; -import { ChatAgentLocation, IChatAgentService } from '../../../../chat/common/chatAgents.js'; +import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; +import { ChatAgentLocation } from '../../../../chat/common/constants.js'; +import { ChatContextKeys } from '../../../../chat/common/chatContextKeys.js'; import { IChatRequestPasteVariableEntry } from '../../../../chat/common/chatModel.js'; import { chatVariableLeader } from '../../../../chat/common/chatParserTypes.js'; import { NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT, NOTEBOOK_CELL_OUTPUT_MIMETYPE } from '../../../common/notebookContextKeys.js'; @@ -249,6 +251,7 @@ registerAction2(class CopyCellOutputAction extends Action2 { }, category: NOTEBOOK_ACTIONS_CATEGORY, icon: icons.copyIcon, + precondition: ChatContextKeys.enabled }); } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index fd3cd47d678..2296896d9d9 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -29,7 +29,7 @@ import { localize } from '../../../../../../nls.js'; import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; -import { ChatAgentLocation } from '../../../../chat/common/chatAgents.js'; +import { ChatAgentLocation } from '../../../../chat/common/constants.js'; import { ChatModel, IChatModel } from '../../../../chat/common/chatModel.js'; import { IChatService } from '../../../../chat/common/chatService.js'; import { countWords } from '../../../../chat/common/chatWordCounter.js'; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts index 1a31b09e0e7..8da4d2c230d 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts @@ -21,7 +21,7 @@ import { Range } from '../../../../../../editor/common/core/range.js'; import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; import { DetailedLineRangeMapping } from '../../../../../../editor/common/diff/rangeMapping.js'; import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../../../scm/common/quickDiff.js'; -import { INotebookOriginalCellModelFactory } from '../../contrib/chatEdit/notebookOriginalCellModelFactory.js'; +import { INotebookOriginalCellModelFactory } from './notebookOriginalCellModelFactory.js'; //TODO: allow client to set read-only - chateditsession should set read-only while making changes export class NotebookCellDiffDecorator extends DisposableStore { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts index 2d9733f68ec..b75579cab13 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts @@ -46,6 +46,10 @@ export class NotebookDeletedCellDecorator extends Disposable implements INoteboo if (!info) { return; } + if (info.previousIndex === -1) { + // deleted cell is before the first real cell + return 0; + } const cells = this._notebookEditor.getCellsInRange({ start: info.previousIndex, end: info.previousIndex + 1 }); if (!cells.length) { return this._notebookEditor.getLayoutInfo().height + info.offset; @@ -65,7 +69,7 @@ export class NotebookDeletedCellDecorator extends Disposable implements INoteboo const info = this.deletedCellInfos.get(deletedIndex); if (info) { - const prevIndex = info.previousIndex; + const prevIndex = info.previousIndex === -1 ? 0 : info.previousIndex; this._notebookEditor.setFocus({ start: prevIndex, end: prevIndex }); this._notebookEditor.setSelections([{ start: prevIndex, end: prevIndex }]); } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInlineDiff.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInlineDiff.ts index 0b1668d0711..c0973dd6f86 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInlineDiff.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInlineDiff.ts @@ -10,13 +10,17 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; import { INotebookEditorWorkerService } from '../../../common/services/notebookWorkerService.js'; -import { CellDiffInfo, computeDiff } from '../notebookDiffViewModel.js'; +import { CellDiffInfo } from '../notebookDiffViewModel.js'; import { INotebookEditorContribution, INotebookEditor } from '../../notebookBrowser.js'; import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; import { NotebookCellDiffDecorator } from './notebookCellDiffDecorator.js'; import { NotebookDeletedCellDecorator } from './notebookDeletedCellDecorator.js'; import { NotebookInsertedCellDecorator } from './notebookInsertedCellDecorator.js'; import { INotebookLoggingService } from '../../../common/notebookLoggingService.js'; +import { computeDiff } from '../../../common/notebookDiff.js'; +import { InstantiationType, registerSingleton } from '../../../../../../platform/instantiation/common/extensions.js'; +import { INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceFactory } from './notebookOriginalModelRefFactory.js'; +import { INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory } from './notebookOriginalCellModelFactory.js'; export class NotebookInlineDiffDecorationContribution extends Disposable implements INotebookEditorContribution { static ID: string = 'workbench.notebook.inlineDiffDecoration'; @@ -162,3 +166,5 @@ export class NotebookInlineDiffDecorationContribution extends Disposable impleme } registerNotebookContribution(NotebookInlineDiffDecorationContribution.ID, NotebookInlineDiffDecorationContribution); +registerSingleton(INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceFactory, InstantiationType.Delayed); +registerSingleton(INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.ts new file mode 100644 index 00000000000..f2993cbb42e --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { CellDiffInfo } from '../notebookDiffViewModel.js'; +import { INotebookEditor } from '../../notebookBrowser.js'; +import { CellKind } from '../../../common/notebookCommon.js'; +import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; + +export class NotebookModifiedCellDecorator extends Disposable { + private readonly decorators = this._register(new DisposableStore()); + constructor( + private readonly notebookEditor: INotebookEditor, + ) { + super(); + } + + public apply(diffInfo: CellDiffInfo[]) { + const model = this.notebookEditor.textModel; + if (!model) { + return; + } + + const modifiedMarkdownCells: NotebookCellTextModel[] = []; + for (const diff of diffInfo) { + if (diff.type === 'modified') { + const cell = model.cells[diff.modifiedCellIndex]; + if (cell.cellKind === CellKind.Markup) { + modifiedMarkdownCells.push(cell); + } + } + } + + const ids = this.notebookEditor.deltaCellDecorations([], modifiedMarkdownCells.map(cell => ({ + handle: cell.handle, + options: { outputClassName: 'nb-insertHighlight' } + }))); + + this.clear(); + this.decorators.add(toDisposable(() => { + if (!this.notebookEditor.isDisposed) { + this.notebookEditor.deltaCellDecorations(ids, []); + } + })); + } + public clear() { + this.decorators.clear(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalCellModelFactory.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalCellModelFactory.ts similarity index 100% rename from src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalCellModelFactory.ts rename to src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalCellModelFactory.ts diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalModelRefFactory.ts similarity index 100% rename from src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts rename to src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalModelRefFactory.ts diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index 81689f503bd..639e2fcd80d 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -289,6 +289,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } async toggleInlineView(): Promise { + this._layoutCancellationTokenSource?.dispose(); + this._inlineView = !this._inlineView; if (!this._lastLayoutProperties) { @@ -302,6 +304,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this.layout(this._lastLayoutProperties?.dimension, this._lastLayoutProperties?.position); this.inlineDiffWidget?.hide(); } + + this._layoutCancellationTokenSource = new CancellationTokenSource(); + this.updateLayout(this._layoutCancellationTokenSource.token); } protected createEditor(parent: HTMLElement): void { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts index 8ec5ec7691c..6ef165a4821 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { IDiffResult, IDiffChange } from '../../../../../base/common/diff/diff.js'; +import { IDiffResult } from '../../../../../base/common/diff/diff.js'; import { Emitter, type IValueWithChangeEvent } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore, dispose } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; @@ -17,11 +17,12 @@ import { DiffElementCellViewModelBase, DiffElementPlaceholderViewModel, IDiffEle import { NotebookDiffEditorEventDispatcher } from './eventDispatcher.js'; import { INotebookDiffViewModel, INotebookDiffViewModelUpdateEvent, NOTEBOOK_DIFF_ITEM_DIFF_STATE, NOTEBOOK_DIFF_ITEM_KIND } from './notebookDiffEditorBrowser.js'; import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; -import { CellUri, INotebookDiffEditorModel, INotebookDiffResult } from '../../common/notebookCommon.js'; +import { CellUri, INotebookDiffEditorModel } from '../../common/notebookCommon.js'; import { INotebookService } from '../../common/notebookService.js'; import { INotebookEditorWorkerService } from '../../common/services/notebookWorkerService.js'; import { IDiffEditorHeightCalculatorService } from './editorHeightCalculator.js'; import { raceCancellation } from '../../../../../base/common/async.js'; +import { computeDiff } from '../../common/notebookDiff.js'; export class NotebookDiffViewModel extends Disposable implements INotebookDiffViewModel, IValueWithChangeEvent { private readonly placeholderAndRelatedCells = new Map(); @@ -415,62 +416,7 @@ export type CellDiffInfo = { modifiedCellIndex: number; type: 'insert'; }; -export function computeDiff(originalModel: NotebookTextModel, modifiedModel: NotebookTextModel, diffResult: INotebookDiffResult) { - const cellChanges = diffResult.cellsDiff.changes; - const cellDiffInfo: CellDiffInfo[] = []; - let originalCellIndex = 0; - let modifiedCellIndex = 0; - let firstChangeIndex = -1; - - for (let i = 0; i < cellChanges.length; i++) { - const change = cellChanges[i]; - // common cells - - for (let j = 0; j < change.originalStart - originalCellIndex; j++) { - const originalCell = originalModel.cells[originalCellIndex + j]; - const modifiedCell = modifiedModel.cells[modifiedCellIndex + j]; - if (originalCell.getHashValue() === modifiedCell.getHashValue()) { - cellDiffInfo.push({ - originalCellIndex: originalCellIndex + j, - modifiedCellIndex: modifiedCellIndex + j, - type: 'unchanged' - }); - } else { - if (firstChangeIndex === -1) { - firstChangeIndex = cellDiffInfo.length; - } - cellDiffInfo.push({ - originalCellIndex: originalCellIndex + j, - modifiedCellIndex: modifiedCellIndex + j, - type: 'modified' - }); - } - } - - const modifiedLCS = computeModifiedLCS(change, originalModel, modifiedModel); - if (modifiedLCS.length && firstChangeIndex === -1) { - firstChangeIndex = cellDiffInfo.length; - } - - cellDiffInfo.push(...modifiedLCS); - originalCellIndex = change.originalStart + change.originalLength; - modifiedCellIndex = change.modifiedStart + change.modifiedLength; - } - - for (let i = originalCellIndex; i < originalModel.cells.length; i++) { - cellDiffInfo.push({ - originalCellIndex: i, - modifiedCellIndex: i - originalCellIndex + modifiedCellIndex, - type: 'unchanged' - }); - } - - return { - cellDiffInfo, - firstChangeIndex - }; -} function isEqual(cellDiffInfo: CellDiffInfo[], viewModels: IDiffElementViewModelBase[], model: INotebookDiffEditorModel) { if (cellDiffInfo.length !== viewModels.length) { return false; @@ -510,40 +456,6 @@ function isEqual(cellDiffInfo: CellDiffInfo[], viewModels: IDiffElementViewModel return true; } - -function computeModifiedLCS(change: IDiffChange, originalModel: NotebookTextModel, modifiedModel: NotebookTextModel) { - const result: CellDiffInfo[] = []; - // modified cells - const modifiedLen = Math.min(change.originalLength, change.modifiedLength); - - for (let j = 0; j < modifiedLen; j++) { - const isTheSame = originalModel.cells[change.originalStart + j].equal(modifiedModel.cells[change.modifiedStart + j]); - result.push({ - originalCellIndex: change.originalStart + j, - modifiedCellIndex: change.modifiedStart + j, - type: isTheSame ? 'unchanged' : 'modified' - }); - } - - for (let j = modifiedLen; j < change.originalLength; j++) { - // deletion - result.push({ - originalCellIndex: change.originalStart + j, - type: 'delete' - }); - } - - for (let j = modifiedLen; j < change.modifiedLength; j++) { - result.push({ - modifiedCellIndex: change.modifiedStart + j, - type: 'insert' - }); - } - - return result; -} - - export abstract class NotebookMultiDiffEditorItem extends MultiDiffEditorItem { constructor( originalUri: URI | undefined, diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellStatusBar.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellStatusBar.css index 8f5eb5dc052..64dd58eecd2 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookCellStatusBar.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellStatusBar.css @@ -68,3 +68,11 @@ .monaco-workbench .notebookOverlay .cell-statusbar-container.is-active-cell .cell-status-item-show-when-active { display: initial; } + +/* Ensure execution status icons always maintain their themed colors */ +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-statusbar-container .cell-status-item .codicon-notebook-state-success { + color: var(--vscode-notebookStatusSuccessIcon-foreground); +} +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-statusbar-container .cell-status-item .codicon-notebook-state-error { + color: var(--vscode-notebookStatusErrorIcon-foreground); +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index c1cb89476ff..10a50d99cdf 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -105,9 +105,6 @@ import './contrib/notebookVariables/notebookInlineVariables.js'; // Diff Editor Contribution import './diff/notebookDiffActions.js'; -// Chat Edit Contributions -import './contrib/chatEdit/contribution.js'; - // Services import { editorOptionsRegistry } from '../../../../editor/common/config/editorOptions.js'; import { NotebookExecutionStateService } from './services/notebookExecutionStateServiceImpl.js'; @@ -135,8 +132,6 @@ import { NotebookMultiDiffEditorInput } from './diff/notebookMultiDiffEditorInpu import { getFormattedMetadataJSON } from '../common/model/notebookCellTextModel.js'; import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from './viewModel/notebookOutlineEntryFactory.js'; import { getFormattedNotebookMetadataJSON } from '../common/model/notebookMetadataTextModel.js'; -import { INotebookSynchronizerService } from '../common/notebookSynchronizerService.js'; -import { NotebookSynchronizerService } from './contrib/chatEdit/notebookSynchronizerService.js'; /*--------------------------------------------------------------------------------------------- */ @@ -882,7 +877,6 @@ registerSingleton(INotebookKeymapService, NotebookKeymapService, InstantiationTy registerSingleton(INotebookLoggingService, NotebookLoggingService, InstantiationType.Delayed); registerSingleton(INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory, InstantiationType.Delayed); registerSingleton(INotebookOutlineEntryFactory, NotebookOutlineEntryFactory, InstantiationType.Delayed); -registerSingleton(INotebookSynchronizerService, NotebookSynchronizerService, InstantiationType.Delayed); const schemas: IJSONSchemaMap = {}; function isConfigurationPropertySchema(x: IConfigurationPropertySchema | { [path: string]: IConfigurationPropertySchema }): x is IConfigurationPropertySchema { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 7234b9b4143..b289cdb6142 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -332,6 +332,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane, I } this._handlePerfMark(perf, input, model.notebook); + this._onDidChangeControl.fire(); } catch (e) { this.logService.warn('NotebookEditorWidget#setInput failed', e); if (isEditorOpenError(e)) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 1f078d4222b..9e0b6f8cf18 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -856,7 +856,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${cellBottomMargin}px; }`); styleSheets.push(` - .notebookOverlay .monaco-list .monaco-list-row:has(+ .monaco-list-row.selected) .cell-focus-indicator-bottom { + .notebookOverlay .monaco-list.selection-multiple .monaco-list-row:has(+ .monaco-list-row.selected) .cell-focus-indicator-bottom { height: ${bottomToolbarGap + cellBottomMargin}px; } `); @@ -1614,6 +1614,28 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } })); + store.add(cell.onCellDecorationsChanged(e => { + e.added.forEach(options => { + if (options.className) { + this.deltaCellContainerClassNames(cell.id, [options.className], []); + } + + if (options.outputClassName) { + this.deltaCellContainerClassNames(cell.id, [options.outputClassName], []); + } + }); + + e.removed.forEach(options => { + if (options.className) { + this.deltaCellContainerClassNames(cell.id, [], [options.className]); + } + + if (options.outputClassName) { + this.deltaCellContainerClassNames(cell.id, [], [options.outputClassName]); + } + }); + })); + return store; } @@ -2321,6 +2343,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD //#region Cell operations/layout API private _pendingLayouts: WeakMap | null = new WeakMap(); + private _layoutDisposables: Set = new Set(); async layoutNotebookCell(cell: ICellViewModel, height: number, context?: CellLayoutContext): Promise { this._debug('layout cell', cell.handle, height); const viewIndex = this._list.getViewIndex(cell); @@ -2353,6 +2376,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return; } + const pendingLayout = this._pendingLayouts?.get(cell); this._pendingLayouts?.delete(cell); if (!this.hasEditorFocus()) { @@ -2371,15 +2395,21 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._list.updateElementHeight2(cell, height); deferred.complete(undefined); + if (pendingLayout) { + pendingLayout.dispose(); + this._layoutDisposables.delete(pendingLayout); + } }; if (this._list.inRenderingTransaction) { const layoutDisposable = DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this.getDomNode()), doLayout); - this._pendingLayouts?.set(cell, toDisposable(() => { + const disposable = toDisposable(() => { layoutDisposable.dispose(); deferred.complete(undefined); - })); + }); + this._pendingLayouts?.set(cell, disposable); + this._layoutDisposables.add(disposable); } else { doLayout(); } @@ -3245,6 +3275,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._webview?.dispose(); this._webview = null; + this._layoutDisposables.forEach(d => d.dispose()); + this.notebookEditorService.removeNotebookEditor(this); dispose(this._contributions.values()); this._contributions.clear(); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index b1670958f9f..f2925b1b8c8 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -212,7 +212,7 @@ export class NotebookProviderInfoStore extends Disposable { // untitled notebooks are disposed when they get saved. we should not hold a reference // to such a disposed notebook and therefore dispose the reference as well - ref.object.notebook.onWillDispose(() => { + Event.once(ref.object.notebook.onWillDispose)(() => { ref.dispose(); }); @@ -871,6 +871,11 @@ export class NotebookService extends Disposable implements INotebookService { } hasSupportedNotebooks(resource: URI): boolean { + if (this._models.has(resource)) { + // it might be untitled + return true; + } + const contribution = this.notebookProviderInfoStore.getContributedNotebook(resource); if (!contribution.length) { return false; diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts index 4a0527917e6..d45bdc91ad1 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts @@ -65,6 +65,7 @@ class WorkerManager extends Disposable { // this._lastWorkerUsedTime = (new Date()).getTime(); if (!this._editorWorkerClient) { this._editorWorkerClient = new NotebookWorkerClient(this._notebookService, this._modelService); + this._register(this._editorWorkerClient); } return Promise.resolve(this._editorWorkerClient); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts index 98340a20af7..9068b002c64 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts @@ -59,22 +59,12 @@ export class CellDecorations extends CellContentPart { e.added.forEach(options => { if (options.className && this.currentCell) { this.rootContainer.classList.add(options.className); - this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [options.className], []); - } - - if (options.outputClassName && this.currentCell) { - this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [options.outputClassName], []); } }); e.removed.forEach(options => { if (options.className && this.currentCell) { this.rootContainer.classList.remove(options.className); - this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [], [options.className]); - } - - if (options.outputClassName && this.currentCell) { - this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [], [options.outputClassName]); } }); })); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts index f58ff0aa23f..a23d2f8b28e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts @@ -79,7 +79,7 @@ export class CellEditorStatusBar extends CellContentPart { readonly showHover = (options: IHoverDelegateOptions) => { options.position = options.position ?? {}; options.position.hoverPosition = HoverPosition.ABOVE; - return hoverService.showHover(options); + return hoverService.showInstantHover(options); }; readonly placement = 'element'; diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index fdeaf59ad84..eea24dd3d15 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -1036,7 +1036,7 @@ export class NotebookCellList extends WorkbenchList implements ID } const editorAttachedPromise = new Promise((resolve, reject) => { - element.onDidChangeEditorAttachState(() => { + Event.once(element.onDidChangeEditorAttachState)(() => { element.editorAttached ? resolve() : reject(); }); }); @@ -1168,7 +1168,8 @@ export class NotebookCellList extends WorkbenchList implements ID const wrapperBottom = this.getViewScrollBottom(); if (offset < scrollTop || offset > wrapperBottom) { - this.view.setScrollTop(offset - this.view.renderHeight / 2); + const newTop = Math.max(0, offset - this.view.renderHeight / 2); + this.view.setScrollTop(newTop); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index f33e4a2dfbe..21e6cf46c13 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -404,6 +404,10 @@ export class BackLayerWebView extends Themable { background-color: var(--theme-notebook-symbol-highlight-background); } + #container .markup > div.nb-insertHighlight { + background-color: var(--vscode-diffEditor-insertedLineBackground, var(--vscode-diffEditor-insertedTextBackground)); + } + #container .nb-symbolHighlight .output_container .output { background-color: var(--theme-notebook-symbol-highlight-background); } @@ -790,7 +794,8 @@ export class BackLayerWebView extends Themable { 'workbench.action.openSettings', '_notebook.selectKernel', // TODO@rebornix explore open output channel with name command - 'jupyter.viewOutput' + 'jupyter.viewOutput', + 'jupyter.createPythonEnvAndSelectController', ], }); return; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 6c0a4f516d3..84589a5ea0b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -84,7 +84,7 @@ export class NotebookCellListDelegate extends Disposable implements IListVirtual } } -abstract class AbstractCellRenderer { +abstract class AbstractCellRenderer extends Disposable { protected readonly editorOptions: CellEditorOptions; constructor( @@ -99,11 +99,12 @@ abstract class AbstractCellRenderer { language: string, protected dndController: CellDragAndDropController | undefined ) { - this.editorOptions = new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(language), this.notebookEditor.notebookOptions, configurationService); + super(); + this.editorOptions = this._register(new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(language), this.notebookEditor.notebookOptions, configurationService)); } - dispose() { - this.editorOptions.dispose(); + override dispose() { + super.dispose(); this.dndController = undefined; } } @@ -199,7 +200,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen editorContainer, foldingIndicator, templateDisposables, - elementDisposables: new DisposableStore(), + elementDisposables: templateDisposables.add(new DisposableStore()), cellParts, toJSON: () => { return {}; } }; @@ -225,7 +226,6 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen } disposeTemplate(templateData: MarkdownCellRenderTemplate): void { - templateData.elementDisposables.dispose(); templateData.templateDisposables.dispose(); } @@ -366,7 +366,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende outputShowMoreContainer, editor, templateDisposables, - elementDisposables: new DisposableStore(), + elementDisposables: templateDisposables.add(new DisposableStore()), cellParts, toJSON: () => { return {}; } }; @@ -397,7 +397,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } disposeTemplate(templateData: CodeCellRenderTemplate): void { - templateData.templateDisposables.clear(); + templateData.templateDisposables.dispose(); } disposeElement(element: ICellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index d59ee4f47e5..ea8227a1a69 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts @@ -47,7 +47,6 @@ export class NotebookStickyLine extends Disposable { this.toggleFoldRange(currentFoldingState); } })); - } private toggleFoldRange(currentState: CellFoldingState) { @@ -212,13 +211,17 @@ export class NotebookStickyScroll extends Disposable { await notebookCellOutline.computeFullSymbols(CancellationToken.None); // Initial content update - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight())); + const computed = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); + this.updateContent(computed); // Set up outline change listener this._disposables.add(notebookCellOutline.onDidChange(() => { - const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); - if (!this.compareStickyLineMaps(recompute, this.currentStickyLines)) { - this.updateContent(recompute); + const computed = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); + if (!this.compareStickyLineMaps(computed, this.currentStickyLines)) { + this.updateContent(computed); + } else { + // if we don't end up updating the content, we need to avoid leaking the map + this.disposeStickyLineMap(computed); } })); @@ -226,21 +229,36 @@ export class NotebookStickyScroll extends Disposable { this._disposables.add(this.notebookEditor.onDidAttachViewModel(async () => { // ensure recompute symbols when view model changes -- could be missed if outline is closed await notebookCellOutline.computeFullSymbols(CancellationToken.None); - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight())); + + const computed = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); + this.updateContent(computed); })); this._disposables.add(this.notebookEditor.onDidScroll(() => { const d = new Delayer(100); d.trigger(() => { d.dispose(); - const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); - if (!this.compareStickyLineMaps(recompute, this.currentStickyLines)) { - this.updateContent(recompute); + + const computed = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); + if (!this.compareStickyLineMaps(computed, this.currentStickyLines)) { + this.updateContent(computed); + } else { + // if we don't end up updating the content, we need to avoid leaking the map + this.disposeStickyLineMap(computed); } }); })); } + // Add helper method to dispose a map of sticky lines + private disposeStickyLineMap(map: Map) { + map.forEach(value => { + if (value.line) { + value.line.dispose(); + } + }); + } + // take in an cell index, and get the corresponding outline entry static getVisibleOutlineEntry(visibleIndex: number, notebookOutlineEntries: OutlineEntry[]): OutlineEntry | undefined { let left = 0; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts index d2467be2d7e..0cee65351d0 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts @@ -72,6 +72,7 @@ export class NotebookEditorContextKeys { dispose(): void { this._disposables.dispose(); this._viewModelDisposables.dispose(); + this._selectedKernelDisposables.dispose(); this._notebookKernelCount.reset(); this._notebookKernelSourceCount.reset(); this._interruptibleKernel.reset(); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 3d2c44c67ec..b8e8b2af29c 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -34,6 +34,7 @@ import { IOpenerService } from '../../../../../platform/opener/common/opener.js' import { INotebookTextModel } from '../../common/notebookCommon.js'; import { SELECT_KERNEL_ID } from '../controller/coreActions.js'; import { EnablementState } from '../../../../services/extensionManagement/common/extensionManagement.js'; +import { areSameExtensions } from '../../../../../platform/extensionManagement/common/extensionManagementUtil.js'; type KernelPick = IQuickPickItem & { kernel: INotebookKernel }; function isKernelPick(item: QuickPickInput): item is KernelPick { @@ -293,7 +294,8 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { const extension = (await extensionWorkbenchService.getExtensions([{ id: extId }], CancellationToken.None))[0]; if (extension.enablementState === EnablementState.DisabledGlobally || extension.enablementState === EnablementState.DisabledWorkspace || extension.enablementState === EnablementState.DisabledByEnvironment) { extensionsToEnable.push(extension); - } else { + } else if (!extensionWorkbenchService.installed.some(e => areSameExtensions(e.identifier, extension.identifier))) { + // Install this extension only if it hasn't already been installed. const canInstall = await extensionWorkbenchService.canInstall(extension); if (canInstall === true) { extensionsToInstall.push(extension); @@ -307,7 +309,7 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { extension, { installPreReleaseVersion: isInsiders ?? false, - context: { skipWalkthrough: true } + context: { skipWalkthrough: true }, }, ProgressLocation.Notification ); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts index 2b2bd8bd744..b4c6ca0c38e 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts @@ -140,11 +140,13 @@ export class NotebooKernelActionViewItem extends ActionViewItem { @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, @INotebookKernelHistoryService private readonly _notebookKernelHistoryService: INotebookKernelHistoryService, ) { + const action = new Action('fakeAction', undefined, ThemeIcon.asClassName(selectKernelIcon), true, (event) => actualAction.run(event)); super( undefined, - new Action('fakeAction', undefined, ThemeIcon.asClassName(selectKernelIcon), true, (event) => actualAction.run(event)), + action, { ...options, label: false, icon: true } ); + this._register(action); this._register(_editor.onDidChangeModel(this._update, this)); this._register(_notebookKernelService.onDidAddKernel(this._update, this)); this._register(_notebookKernelService.onDidRemoveKernel(this._update, this)); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 8f84a01bd2f..3970dd6be5f 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -209,9 +209,6 @@ export class NotebookCellTextModel extends Disposable implements ICell { this._outputs = outputs.map(op => new NotebookCellOutputTextModel(op)); this._metadata = metadata ?? {}; this._internalMetadata = internalMetadata ?? {}; - this._internalMetadata.cellId = this._internalMetadata.cellId ?? - this._metadata.id as string ?? - uri.fragment; } enableAutoLanguageDetection() { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index d87906fa713..1558447f16e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -19,7 +19,6 @@ import { ISplice } from '../../../../base/common/sequence.js'; import { ThemeColor } from '../../../../base/common/themables.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { Range } from '../../../../editor/common/core/range.js'; -import { ILineChange } from '../../../../editor/common/diff/legacyLinesDiffComputer.js'; import * as editorCommon from '../../../../editor/common/editorCommon.js'; import { Command, WorkspaceEditMetadata } from '../../../../editor/common/languages.js'; import { IReadonlyTextBuffer, ITextModel } from '../../../../editor/common/model.js'; @@ -121,6 +120,11 @@ export interface NotebookCellMetadata { } export interface NotebookCellInternalMetadata { + /** + * Used only for diffing of Notebooks. + * This is not persisted and generally useful only when diffing two notebooks. + * Useful only after we've manually matched a few cells together so we know which cells are matching. + */ cellId?: string; executionId?: string; executionOrder?: number; @@ -955,7 +959,6 @@ export interface INotebookCellStatusBarItemProvider { export interface INotebookDiffResult { cellsDiff: IDiffResult; metadataChanged: boolean; - linesDiff?: { originalCellhandle: number; modifiedCellhandle: number; lineChanges: ILineChange[] }[]; } export interface INotebookCellStatusBarItem { diff --git a/src/vs/workbench/contrib/notebook/common/notebookDiff.ts b/src/vs/workbench/contrib/notebook/common/notebookDiff.ts new file mode 100644 index 00000000000..fb8844cfc13 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookDiff.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDiffChange } from '../../../../base/common/diff/diff.js'; +import { CellKind, INotebookDiffResult } from './notebookCommon.js'; + +export type CellDiffInfo = { + originalCellIndex: number; + modifiedCellIndex: number; + type: 'unchanged' | 'modified'; +} | +{ + originalCellIndex: number; + type: 'delete'; +} | +{ + modifiedCellIndex: number; + type: 'insert'; +}; + +interface ICell { + cellKind: CellKind; + getHashValue(): number; + equal(cell: ICell): boolean; +} +// interface INotebookDiffResult { +// cellsDiff: IDiffResult; +// metadataChanged: boolean; +// } + +export function computeDiff(originalModel: { readonly cells: readonly ICell[] }, modifiedModel: { readonly cells: readonly ICell[] }, diffResult: INotebookDiffResult) { + const cellChanges = diffResult.cellsDiff.changes; + const cellDiffInfo: CellDiffInfo[] = []; + let originalCellIndex = 0; + let modifiedCellIndex = 0; + + let firstChangeIndex = -1; + + for (let i = 0; i < cellChanges.length; i++) { + const change = cellChanges[i]; + // common cells + + for (let j = 0; j < change.originalStart - originalCellIndex; j++) { + const originalCell = originalModel.cells[originalCellIndex + j]; + const modifiedCell = modifiedModel.cells[modifiedCellIndex + j]; + if (originalCell.getHashValue() === modifiedCell.getHashValue()) { + cellDiffInfo.push({ + originalCellIndex: originalCellIndex + j, + modifiedCellIndex: modifiedCellIndex + j, + type: 'unchanged' + }); + } else { + if (firstChangeIndex === -1) { + firstChangeIndex = cellDiffInfo.length; + } + cellDiffInfo.push({ + originalCellIndex: originalCellIndex + j, + modifiedCellIndex: modifiedCellIndex + j, + type: 'modified' + }); + } + } + + const modifiedLCS = computeModifiedLCS(change, originalModel, modifiedModel); + if (modifiedLCS.length && firstChangeIndex === -1) { + firstChangeIndex = cellDiffInfo.length; + } + + cellDiffInfo.push(...modifiedLCS); + originalCellIndex = change.originalStart + change.originalLength; + modifiedCellIndex = change.modifiedStart + change.modifiedLength; + } + + for (let i = originalCellIndex; i < originalModel.cells.length; i++) { + cellDiffInfo.push({ + originalCellIndex: i, + modifiedCellIndex: i - originalCellIndex + modifiedCellIndex, + type: 'unchanged' + }); + } + + return { + cellDiffInfo, + firstChangeIndex + }; +} + +function computeModifiedLCS(change: IDiffChange, originalModel: { readonly cells: readonly ICell[] }, modifiedModel: { readonly cells: readonly ICell[] }) { + const result: CellDiffInfo[] = []; + // modified cells + const modifiedLen = Math.min(change.originalLength, change.modifiedLength); + + for (let j = 0; j < modifiedLen; j++) { + const originalCell = originalModel.cells[change.originalStart + j]; + const modifiedCell = modifiedModel.cells[change.modifiedStart + j]; + if (originalCell.cellKind !== modifiedCell.cellKind) { + result.push({ + originalCellIndex: change.originalStart + j, + type: 'delete' + }); + result.push({ + modifiedCellIndex: change.modifiedStart + j, + type: 'insert' + }); + } else { + const isTheSame = originalCell.equal(modifiedCell); + result.push({ + originalCellIndex: change.originalStart + j, + modifiedCellIndex: change.modifiedStart + j, + type: isTheSame ? 'unchanged' : 'modified' + }); + } + } + + for (let j = modifiedLen; j < change.originalLength; j++) { + // deletion + result.push({ + originalCellIndex: change.originalStart + j, + type: 'delete' + }); + } + + for (let j = modifiedLen; j < change.modifiedLength; j++) { + result.push({ + modifiedCellIndex: change.modifiedStart + j, + type: 'insert' + }); + } + + return result; +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 68b112f2238..d0e9724f4df 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -27,7 +27,6 @@ import { IFileWorkingCopyManager } from '../../../services/workingCopy/common/fi import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopySaveEvent, StoredFileWorkingCopyState } from '../../../services/workingCopy/common/storedFileWorkingCopy.js'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory } from '../../../services/workingCopy/common/untitledFileWorkingCopy.js'; import { WorkingCopyCapabilities } from '../../../services/workingCopy/common/workingCopy.js'; -import { INotebookSynchronizerService } from './notebookSynchronizerService.js'; //#region --- simple content provider @@ -56,7 +55,6 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE private readonly _workingCopyManager: IFileWorkingCopyManager, scratchpad: boolean, @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService, - @INotebookSynchronizerService private readonly _notebookSynchronizerService: INotebookSynchronizerService ) { super(); @@ -122,9 +120,6 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE async revert(options?: IRevertOptions): Promise { assertType(this.isResolved()); - if (this._workingCopy) { - await this._notebookSynchronizerService.revert(this._workingCopy); - } return this._workingCopy!.revert(options); } @@ -141,7 +136,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } else { this._workingCopy = await this._workingCopyManager.resolve({ untitledResource: this.resource, isScratchpad: this.scratchPad }); } - this._workingCopy.onDidRevert(() => this._onDidRevertUntitled.fire()); + this._register(this._workingCopy.onDidRevert(() => this._onDidRevertUntitled.fire())); } else { this._workingCopy = await this._workingCopyManager.resolve(this.resource, { limits: options?.limits, diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index a21a95750b0..862ca633005 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -61,6 +61,15 @@ class NotebookModelReferenceCollection extends ReferenceCollection { // Untrack as being disposed this.modelsToDispose.delete(key); @@ -186,7 +195,7 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes const suffix = NotebookProviderInfo.possibleFileEnding(info.selectors) ?? ''; for (let counter = 1; ; counter++) { const candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}${suffix}`, query: notebookType }); - if (!this._notebookService.getNotebookTextModel(candidate)) { + if (!this._notebookService.getNotebookTextModel(candidate) && !this._data.isListeningToModel(candidate)) { return candidate; } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts index ef560ea43fc..ab14d89413d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -33,7 +33,7 @@ export class NotebookProviderInfo { readonly priority: RegisteredEditorPriority; readonly providerDisplayName: string; - private _selectors: NotebookSelector[]; + public _selectors: NotebookSelector[]; get selectors() { return this._selectors; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookSynchronizerService.ts b/src/vs/workbench/contrib/notebook/common/notebookSynchronizerService.ts deleted file mode 100644 index 4fc504655f5..00000000000 --- a/src/vs/workbench/contrib/notebook/common/notebookSynchronizerService.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IStoredFileWorkingCopy } from '../../../services/workingCopy/common/storedFileWorkingCopy.js'; -import { IUntitledFileWorkingCopy } from '../../../services/workingCopy/common/untitledFileWorkingCopy.js'; -import { NotebookFileWorkingCopyModel } from './notebookEditorModel.js'; - -export const INotebookSynchronizerService = createDecorator('notebookSynchronizerService'); - -export interface INotebookSynchronizerService { - readonly _serviceBrand: undefined; - revert(workingCopy: IStoredFileWorkingCopy | IUntitledFileWorkingCopy): Promise; -} diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookCellMatching.ts b/src/vs/workbench/contrib/notebook/common/services/notebookCellMatching.ts new file mode 100644 index 00000000000..8e7375819c6 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookCellMatching.ts @@ -0,0 +1,505 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CellKind } from '../notebookCommon.js'; + + +type EditCount = number; +type OriginalIndex = number; +type ModifiedIndex = number; +type CellEditCountCache = { + modifiedToOriginal: Map>; + originalToModified: Map>; +}; + +type ICell = { + getValue(): string; + getLinesContent(): string[]; + cellKind: CellKind; +}; + +/** + * Given a set of modified cells and original cells, this function will attempt to match the modified cells with the original cells. + * E.g. Assume you have (original on left and modified on right): + * ================= + * Cell A | Cell a + * Cell B | Cell b + * Cell C | Cell d + * Cell D | Cell e + * ================= + * Here we know that `Cell C` has been removed and `Cell e` has been added. + * The mapping from modified to original will be as follows: + * Cell a => Cell A + * Cell b => Cell B + * Cell d => Cell D + * Cell e => + * Cell C in original was not matched, hence it was deleted. + * + * Thus the return value is as follows: + * [ + * { modified: 0, original: 0 }, + * { modified: 1, original: 1 }, + * { modified: 2, original: 3 }, + * { modified: 3, original: -1 }, + * ] + * @returns + */ +export function matchCellBasedOnSimilarties(modifiedCells: ICell[], originalCells: ICell[]): { modified: number; original: number; percentage: number }[] { + const cache: CellEditCountCache = { + modifiedToOriginal: new Map>(), + originalToModified: new Map>(), + }; + const results: { modified: number; original: number; dist: number; percentage: number; possibleOriginal: number }[] = []; + const mappedOriginalCellToModifiedCell = new Map(); + const mappedModifiedIndexes = new Set(); + const originalIndexWithMostEdits = new Map(); + const canOriginalIndexBeMappedToModifiedIndex = (originalIndex: number, value: { editCount: EditCount }) => { + if (mappedOriginalCellToModifiedCell.has(originalIndex)) { + return false; + } + const existingEdits = originalIndexWithMostEdits.get(originalIndex)?.dist ?? Number.MAX_SAFE_INTEGER; + return value.editCount < existingEdits; + }; + const trackMappedIndexes = (modifiedIndex: number, originalIndex: number) => { + mappedOriginalCellToModifiedCell.set(originalIndex, modifiedIndex); + mappedModifiedIndexes.add(modifiedIndex); + }; + + for (let i = 0; i < modifiedCells.length; i++) { + const modifiedCell = modifiedCells[i]; + const { index, editCount: dist, percentage } = computeClosestCell({ cell: modifiedCell, index: i }, originalCells, true, cache, canOriginalIndexBeMappedToModifiedIndex); + if (index >= 0 && dist === 0) { + trackMappedIndexes(i, index); + results.push({ modified: i, original: index, dist, percentage, possibleOriginal: index }); + } else { + originalIndexWithMostEdits.set(index, { dist: dist, modifiedIndex: i }); + results.push({ modified: i, original: -1, dist: dist, percentage, possibleOriginal: index }); + } + } + + results.forEach((result, i) => { + if (result.original >= 0) { + return; + } + + /** + * I.e. Assume you have the following + * ================= + * A a (this has ben matched) + * B b + * C c + * D d (these two have been matched) + * e e + * f f + * ================= + * Just match A => a, B => b, C => c + */ + // Find the next cell that has been matched. + const previousMatchedCell = i > 0 ? results.slice(0, i).reverse().find(r => r.original >= 0) : undefined; + const previousMatchedOriginalIndex = previousMatchedCell?.original ?? -1; + const previousMatchedModifiedIndex = previousMatchedCell?.modified ?? -1; + const matchedCell = results.slice(i + 1).find(r => r.original >= 0); + const unavailableIndexes = new Set(); + const nextMatchedModifiedIndex = results.findIndex((item, idx) => idx > i && item.original >= 0); + const nextMatchedOriginalIndex = nextMatchedModifiedIndex >= 0 ? results[nextMatchedModifiedIndex].original : -1; + // Find the available indexes that we can match with. + // We are only interested in b and c (anything after d is of no use). + originalCells.forEach((_, i) => { + if (mappedOriginalCellToModifiedCell.has(i)) { + unavailableIndexes.add(i); + return; + } + if (matchedCell && i >= matchedCell.original) { + unavailableIndexes.add(i); + } + if (nextMatchedOriginalIndex >= 0 && i > nextMatchedOriginalIndex) { + unavailableIndexes.add(i); + } + }); + + + const modifiedCell = modifiedCells[i]; + /** + * I.e. Assume you have the following + * ================= + * A a (this has ben matched) + * B b + * C c + * D d (these two have been matched) + * e e + * f f + * ================= + * Given that we have a probable match for B => b, we can match it. + */ + if (result.original === -1 && result.possibleOriginal >= 0 && !unavailableIndexes.has(result.possibleOriginal) && canOriginalIndexBeMappedToModifiedIndex(result.possibleOriginal, { editCount: result.dist })) { + trackMappedIndexes(i, result.possibleOriginal); + result.original = result.possibleOriginal; + return; + } + + + /** + * I.e. Assume you have the following + * ================= + * A a (this has ben matched) + * B b + * C c + * D d (these two have been matched) + * ================= + * Its possible that B matches better with c and C matches better with b. + * However given the fact that we have matched A => a and D => d. + * & if the indexes are an exact match. + * I.e. index of D in Modified === index of d in Original, and index of A in Modified === index of a in Original. + * Then this means there are absolutely no modifications. + * Hence we can just assign the indexes as is. + * + * NOTE: For this, we must ensure we have exactly the same number of items on either side. + * I.e. we have B, C remaining in Modified, and b, c remaining in Original. + * Thats 2 Modified items === 2 Original Items. + * If its not the same, then this means something has been deleted/inserted, and we cannot blindly map the indexes. + */ + if (previousMatchedOriginalIndex > 0 && previousMatchedModifiedIndex > 0 && previousMatchedOriginalIndex === previousMatchedModifiedIndex) { + if ((nextMatchedModifiedIndex >= 0 ? nextMatchedModifiedIndex : modifiedCells.length - 1) === (nextMatchedOriginalIndex >= 0 ? nextMatchedOriginalIndex : originalCells.length - 1) && !unavailableIndexes.has(i) && i < originalCells.length) { + const remainingModifiedItems = (nextMatchedModifiedIndex >= 0 ? nextMatchedModifiedIndex : modifiedCells.length) - previousMatchedModifiedIndex; + const remainingOriginalItems = (nextMatchedOriginalIndex >= 0 ? nextMatchedOriginalIndex : originalCells.length) - previousMatchedOriginalIndex; + if (remainingModifiedItems === remainingOriginalItems && modifiedCell.cellKind === originalCells[i].cellKind) { + trackMappedIndexes(i, i); + result.original = i; + return; + } + } + } + /** + * I.e. Assume you have the following + * ================= + * A a (this has ben matched) + * B b + * C c + * D d (these two have been matched) + * e e + * f f + * ================= + * We can now try to match B with b and c and figure out which is best. + * RULE 1. Its possible that B will match best with c, howevber C matches better with c, meaning we should match B with b. + * To do this, we need to see if c has a better match with something else. + */ + // RULE 1 + // Try to find the next best match, but exclucde items that have a better match. + const { index, percentage } = computeClosestCell({ cell: modifiedCell, index: i }, originalCells, false, cache, (originalIndex: number, originalValue: { editCount: EditCount }) => { + if (unavailableIndexes.has(originalIndex)) { + return false; + } + + if (nextMatchedModifiedIndex > 0 || previousMatchedOriginalIndex > 0) { + // See if we have a beter match for this. + const matchesForThisOriginalIndex = cache.originalToModified.get(originalIndex); + if (matchesForThisOriginalIndex && previousMatchedOriginalIndex < originalIndex) { + const betterMatch = Array.from(matchesForThisOriginalIndex).find(([modifiedIndex, value]) => { + if (modifiedIndex === i) { + // This is the same modifeid entry. + return false; + } + if (modifiedIndex >= nextMatchedModifiedIndex) { + // We're only interested in matches that are before the next matched index. + return false; + } + if (mappedModifiedIndexes.has(i)) { + // This has already been matched. + return false; + } + return value.editCount < originalValue.editCount; + }); + if (betterMatch) { + // We do have a better match for this, hence do not use this. + return false; + } + } + } + return !unavailableIndexes.has(originalIndex); + }); + + /** + * I.e. Assume you have the following + * ================= + * A a (this has ben matched) + * B bbbbbbbbbbbbbb + * C cccccccccccccc + * D d (these two have been matched) + * e e + * f f + * ================= + * RULE 1 . Now when attempting to match `bbbbbbbbbbbb` with B, the number of edits is very high and the percentage is also very high. + * Basically majority of the text needs to be changed. + * However if the indexes line up perfectly well, and this is the best match, then use it. + * + * Similarly its possible we're trying to match b with `BBBBBBBBBBBB` and the number of edits is very high, but the indexes line up perfectly well. + * + * RULE 2. However it is also possible that there's a better match with another cell + * Assume we have + * ================= + * AAAA a (this has been matched) + * bbbbbbbb b + * bbbb c + * dddd d (these two have been matched) + * ================= + * In this case if we use the algorithm of (1) above, we'll end up matching bbbb with b, and bbbbbbbb with c. + * But we're not really sure if this is the best match. + * In such cases try to match with the same cell index. + * + */ + // RULE 1 (got a match and the indexes line up perfectly well, use it regardless of the number of edits). + if (index >= 0 && i > 0 && results[i - 1].original === index - 1) { + trackMappedIndexes(i, index); + results[i].original = index; + return; + } + + // RULE 2 + // Here we know that `AAAA => a` + // Check if the previous cell has been matched. + // And if the next modified and next original cells are a match. + const nextOriginalCell = (i > 0 && originalCells.length > results[i - 1].original) ? results[i - 1].original + 1 : -1; + const nextOriginalCellValue = i > 0 && nextOriginalCell >= 0 && nextOriginalCell < originalCells.length ? originalCells[nextOriginalCell].getValue() : undefined; + if (index >= 0 && i > 0 && typeof nextOriginalCellValue === 'string' && !mappedOriginalCellToModifiedCell.has(nextOriginalCell)) { + if (modifiedCell.getValue().includes(nextOriginalCellValue) || nextOriginalCellValue.includes(modifiedCell.getValue())) { + trackMappedIndexes(i, nextOriginalCell); + results[i].original = nextOriginalCell; + return; + } + } + + if (percentage < 90 || (i === 0 && results.length === 1)) { + trackMappedIndexes(i, index); + results[i].original = index; + return; + } + }); + + return results; +} + +function computeClosestCell({ cell, index: cellIndex }: { cell: ICell; index: number }, arr: readonly ICell[], ignoreEmptyCells: boolean, cache: CellEditCountCache, canOriginalIndexBeMappedToModifiedIndex: (originalIndex: number, value: { editCount: EditCount }) => boolean): { index: number; editCount: number; percentage: number } { + let min_edits = Infinity; + let min_index = -1; + for (let i = 0; i < arr.length; i++) { + // Skip cells that are not of the same kind. + if (arr[i].cellKind !== cell.cellKind) { + continue; + } + const str = arr[i].getValue(); + const cacheEntry = cache.modifiedToOriginal.get(cellIndex) ?? new Map(); + const value = cacheEntry.get(i) ?? { editCount: computeNumberOfEdits(cell, arr[i]), }; + cacheEntry.set(i, value); + cache.modifiedToOriginal.set(cellIndex, cacheEntry); + + const originalCacheEntry = cache.originalToModified.get(i) ?? new Map(); + originalCacheEntry.set(cellIndex, value); + cache.originalToModified.set(i, originalCacheEntry); + + if (!canOriginalIndexBeMappedToModifiedIndex(i, value)) { + continue; + } + if (str.length === 0 && ignoreEmptyCells) { + continue; + } + if (str === cell.getValue() && cell.getValue().length > 0) { + return { index: i, editCount: 0, percentage: 0 }; + } + + if (value.editCount < min_edits) { + min_edits = value.editCount; + min_index = i; + } + } + + if (min_index === -1) { + return { index: -1, editCount: Number.MAX_SAFE_INTEGER, percentage: Number.MAX_SAFE_INTEGER }; + } + const percentage = !cell.getValue().length && !arr[min_index].getValue().length ? 0 : (cell.getValue().length ? (min_edits * 100 / cell.getValue().length) : Number.MAX_SAFE_INTEGER); + return { index: min_index, editCount: min_edits, percentage }; +} + +function computeNumberOfEdits(modified: ICell, original: ICell) { + if (modified.getValue() === original.getValue()) { + return 0; + } + + return computeLevenshteinDistance(modified.getValue(), original.getValue()); +} + +/** + * Precomputed equality array for character codes. + */ +const precomputedEqualityArray = new Uint32Array(0x10000); + +/** + * Computes the Levenshtein distance for strings of length <= 32. + * @param firstString - The first string. + * @param secondString - The second string. + * @returns The Levenshtein distance. + */ +const computeLevenshteinDistanceForShortStrings = (firstString: string, secondString: string): number => { + const firstStringLength = firstString.length; + const secondStringLength = secondString.length; + const lastBitMask = 1 << (firstStringLength - 1); + let positiveVector = -1; + let negativeVector = 0; + let distance = firstStringLength; + let index = firstStringLength; + + // Initialize precomputedEqualityArray for firstString + while (index--) { + precomputedEqualityArray[firstString.charCodeAt(index)] |= 1 << index; + } + + // Process each character of secondString + for (index = 0; index < secondStringLength; index++) { + let equalityMask = precomputedEqualityArray[secondString.charCodeAt(index)]; + const combinedVector = equalityMask | negativeVector; + equalityMask |= ((equalityMask & positiveVector) + positiveVector) ^ positiveVector; + negativeVector |= ~(equalityMask | positiveVector); + positiveVector &= equalityMask; + if (negativeVector & lastBitMask) { + distance++; + } + if (positiveVector & lastBitMask) { + distance--; + } + negativeVector = (negativeVector << 1) | 1; + positiveVector = (positiveVector << 1) | ~(combinedVector | negativeVector); + negativeVector &= combinedVector; + } + + // Reset precomputedEqualityArray + index = firstStringLength; + while (index--) { + precomputedEqualityArray[firstString.charCodeAt(index)] = 0; + } + + return distance; +}; + +/** + * Computes the Levenshtein distance for strings of length > 32. + * @param firstString - The first string. + * @param secondString - The second string. + * @returns The Levenshtein distance. + */ +function computeLevenshteinDistanceForLongStrings(firstString: string, secondString: string): number { + const firstStringLength = firstString.length; + const secondStringLength = secondString.length; + const horizontalBitArray = []; + const verticalBitArray = []; + const horizontalSize = Math.ceil(firstStringLength / 32); + const verticalSize = Math.ceil(secondStringLength / 32); + + // Initialize horizontal and vertical bit arrays + for (let i = 0; i < horizontalSize; i++) { + horizontalBitArray[i] = -1; + verticalBitArray[i] = 0; + } + + let verticalIndex = 0; + for (; verticalIndex < verticalSize - 1; verticalIndex++) { + let negativeVector = 0; + let positiveVector = -1; + const start = verticalIndex * 32; + const verticalLength = Math.min(32, secondStringLength) + start; + + // Initialize precomputedEqualityArray for secondString + for (let k = start; k < verticalLength; k++) { + precomputedEqualityArray[secondString.charCodeAt(k)] |= 1 << k; + } + + // Process each character of firstString + for (let i = 0; i < firstStringLength; i++) { + const equalityMask = precomputedEqualityArray[firstString.charCodeAt(i)]; + const previousBit = (horizontalBitArray[(i / 32) | 0] >>> i) & 1; + const matchBit = (verticalBitArray[(i / 32) | 0] >>> i) & 1; + const combinedVector = equalityMask | negativeVector; + const combinedHorizontalVector = ((((equalityMask | matchBit) & positiveVector) + positiveVector) ^ positiveVector) | equalityMask | matchBit; + let positiveHorizontalVector = negativeVector | ~(combinedHorizontalVector | positiveVector); + let negativeHorizontalVector = positiveVector & combinedHorizontalVector; + if ((positiveHorizontalVector >>> 31) ^ previousBit) { + horizontalBitArray[(i / 32) | 0] ^= 1 << i; + } + if ((negativeHorizontalVector >>> 31) ^ matchBit) { + verticalBitArray[(i / 32) | 0] ^= 1 << i; + } + positiveHorizontalVector = (positiveHorizontalVector << 1) | previousBit; + negativeHorizontalVector = (negativeHorizontalVector << 1) | matchBit; + positiveVector = negativeHorizontalVector | ~(combinedVector | positiveHorizontalVector); + negativeVector = positiveHorizontalVector & combinedVector; + } + + // Reset precomputedEqualityArray + for (let k = start; k < verticalLength; k++) { + precomputedEqualityArray[secondString.charCodeAt(k)] = 0; + } + } + + let negativeVector = 0; + let positiveVector = -1; + const start = verticalIndex * 32; + const verticalLength = Math.min(32, secondStringLength - start) + start; + + // Initialize precomputedEqualityArray for secondString + for (let k = start; k < verticalLength; k++) { + precomputedEqualityArray[secondString.charCodeAt(k)] |= 1 << k; + } + + let distance = secondStringLength; + + // Process each character of firstString + for (let i = 0; i < firstStringLength; i++) { + const equalityMask = precomputedEqualityArray[firstString.charCodeAt(i)]; + const previousBit = (horizontalBitArray[(i / 32) | 0] >>> i) & 1; + const matchBit = (verticalBitArray[(i / 32) | 0] >>> i) & 1; + const combinedVector = equalityMask | negativeVector; + const combinedHorizontalVector = ((((equalityMask | matchBit) & positiveVector) + positiveVector) ^ positiveVector) | equalityMask | matchBit; + let positiveHorizontalVector = negativeVector | ~(combinedHorizontalVector | positiveVector); + let negativeHorizontalVector = positiveVector & combinedHorizontalVector; + distance += (positiveHorizontalVector >>> (secondStringLength - 1)) & 1; + distance -= (negativeHorizontalVector >>> (secondStringLength - 1)) & 1; + if ((positiveHorizontalVector >>> 31) ^ previousBit) { + horizontalBitArray[(i / 32) | 0] ^= 1 << i; + } + if ((negativeHorizontalVector >>> 31) ^ matchBit) { + verticalBitArray[(i / 32) | 0] ^= 1 << i; + } + positiveHorizontalVector = (positiveHorizontalVector << 1) | previousBit; + negativeHorizontalVector = (negativeHorizontalVector << 1) | matchBit; + positiveVector = negativeHorizontalVector | ~(combinedVector | positiveHorizontalVector); + negativeVector = positiveHorizontalVector & combinedVector; + } + + // Reset precomputedEqualityArray + for (let k = start; k < verticalLength; k++) { + precomputedEqualityArray[secondString.charCodeAt(k)] = 0; + } + + return distance; +} + +/** + * Computes the Levenshtein distance between two strings. + * @param firstString - The first string. + * @param secondString - The second string. + * @returns The Levenshtein distance. + */ +function computeLevenshteinDistance(firstString: string, secondString: string): number { + if (firstString.length < secondString.length) { + const temp = secondString; + secondString = firstString; + firstString = temp; + } + if (secondString.length === 0) { + return firstString.length; + } + if (firstString.length <= 32) { + return computeLevenshteinDistanceForShortStrings(firstString, secondString); + } + return computeLevenshteinDistanceForLongStrings(firstString, secondString); +} diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts index c5b12612bb8..3ebedc7bab4 100644 --- a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts +++ b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts @@ -2,8 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDiffResult, ISequence, LcsDiff } from '../../../../../base/common/diff/diff.js'; -import { doHash, numberHash } from '../../../../../base/common/hash.js'; +import { IDiffChange, ISequence, LcsDiff } from '../../../../../base/common/diff/diff.js'; +import { doHash, hash, numberHash } from '../../../../../base/common/hash.js'; import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { IRequestHandler, IWorkerServer } from '../../../../../base/common/worker/simpleWorker.js'; @@ -15,10 +15,16 @@ import { MirrorModel } from '../../../../../editor/common/services/textModelSync import { DefaultEndOfLine } from '../../../../../editor/common/model.js'; import { IModelChangedEvent } from '../../../../../editor/common/model/mirrorTextModel.js'; import { filter } from '../../../../../base/common/objects.js'; +import { matchCellBasedOnSimilarties } from './notebookCellMatching.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; +import { DiffChange } from '../../../../../base/common/diff/diffChange.js'; +import { computeDiff } from '../notebookDiff.js'; + +const PREFIX_FOR_UNMATCHED_ORIGINAL_CELLS = `unmatchedOriginalCell`; class MirrorCell { private readonly textModel: MirrorModel; - private _hash?: Promise; + private _hash?: number; public get eol() { return this._eol === '\r\n' ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF; } @@ -46,17 +52,21 @@ class MirrorCell { return this.textModel.getValue(); } - async getComparisonValue(): Promise { + getLinesContent(): string[] { + return this.textModel.getLinesContent(); + } + getComparisonValue(): number { return this._hash ??= this._getHash(); } - private async _getHash() { + private _getHash() { let hashValue = numberHash(104579, 0); hashValue = doHash(this.language, hashValue); hashValue = doHash(this.getValue(), hashValue); hashValue = doHash(this.metadata, hashValue); - hashValue = doHash({ ...this.internalMetadata, 'cellId': '' }, hashValue); + // For purpose of diffing only cellId matters, rest do not + hashValue = doHash(this.internalMetadata?.cellId || '', hashValue); for (const op of this.outputs) { hashValue = doHash(op.metadata, hashValue); for (const output of op.outputs) { @@ -64,15 +74,13 @@ class MirrorCell { } } - // note: hash has not updated within the Promise.all since we must retain order - const digests = await Promise.all(this.outputs.flatMap(op => - op.outputs.map(o => crypto.subtle.digest('sha-1', o.data.buffer)) - )); + const digests = this.outputs.flatMap(op => + op.outputs.map(o => hash(Array.from(o.data.buffer))) + ); for (const digest of digests) { - hashValue = numberHash(new Int32Array(digest)[0], hashValue); + hashValue = numberHash(digest, hashValue); } - return hashValue; } } @@ -147,25 +155,22 @@ class MirrorNotebookDocument { class CellSequence implements ISequence { - static async create(textModel: MirrorNotebookDocument) { - const hashValue = new Int32Array(textModel.cells.length); - await Promise.all(textModel.cells.map(async (c, i) => { - hashValue[i] = await c.getComparisonValue(); - })); + static create(textModel: MirrorNotebookDocument) { + const hashValue = textModel.cells.map(c => c.getComparisonValue()); + return new CellSequence(hashValue); + } + static createWithCellId(cells: MirrorCell[], includeCellContents?: boolean) { + const hashValue = cells.map((c) => { + if (includeCellContents) { + return `${doHash(c.internalMetadata?.cellId, numberHash(104579, 0))}#${c.getComparisonValue()}`; + } else { + return `${doHash(c.internalMetadata?.cellId, numberHash(104579, 0))}}`; + } + }); return new CellSequence(hashValue); } - static async createWithCellId(textModel: MirrorNotebookDocument): Promise> { - const hashValue = new Map(); - await Promise.all(textModel.cells.map(async (c, i) => { - const value = await c.getComparisonValue(); - const id: string = (c.metadata?.id || '') as string; - hashValue.set(id, value); - })); - return hashValue; - } - - constructor(readonly hashValue: Int32Array) { } + constructor(readonly hashValue: number[] | string[]) { } getElements(): string[] | number[] | Int32Array { return this.hashValue; @@ -219,103 +224,258 @@ export class NotebookEditorSimpleWorker implements IRequestHandler, IDisposable const original = this._getModel(originalUrl); const modified = this._getModel(modifiedUrl); - const [originalSeq, modifiedSeq] = await Promise.all([ - CellSequence.create(original), - CellSequence.create(modified), - ]); - - const diff = new LcsDiff(originalSeq, modifiedSeq); - const diffResult = diff.ComputeDiff(false); + const originalModel = new NotebookTextModelFacade(original); + const modifiedModel = new NotebookTextModelFacade(modified); const originalMetadata = filter(original.metadata, key => !original.transientDocumentMetadata[key]); const modifiedMetadata = filter(modified.metadata, key => !modified.transientDocumentMetadata[key]); + const metadataChanged = JSON.stringify(originalMetadata) !== JSON.stringify(modifiedMetadata); + // TODO@DonJayamanne + // In the future we might want to avoid computing LCS of outputs + // That will make this faster. + const originalDiff = new LcsDiff(CellSequence.create(original), CellSequence.create(modified)).ComputeDiff(false); + if (originalDiff.changes.length === 0) { + return { + metadataChanged, + cellsDiff: originalDiff + }; + } + + // This will return the mapping of the cells and what cells were inserted/deleted. + // We do not care much about accuracy of the diff, but care about the mapping of unmodified cells. + // That can be used as anchor points to find the cells that have changed. + // And on cells that have changed, we can use similarity algorithms to find the mapping. + // Eg as mentioned earlier, its possible after similarity algorithms we find that cells weren't inserted/deleted but were just modified. + const cellMapping = computeDiff(originalModel, modifiedModel, { cellsDiff: { changes: originalDiff.changes, quitEarly: false }, metadataChanged: false, }).cellDiffInfo; + + // If we have no insertions/deletions, then this is a good diffing. + if (cellMapping.every(c => c.type === 'modified')) { + return { + metadataChanged, + cellsDiff: originalDiff + }; + } + + let diffUsingCellIds = this.canComputeDiffWithCellIds(original, modified); + if (!diffUsingCellIds) { + /** + * Assume we have cells as follows + * Original Modified + * A A + * B B + * C e + * D F + * E + * F + * + * Using LCS we know easily that A, B cells match. + * Using LCS it would look like C changed to e + * Using LCS D & E were removed. + * + * A human would be able to tell that cell C, D were removed. + * A human can tell that E changed to e because the code in the cells are very similar. + * Note the words `similar`, humans try to match cells based on certain heuristics. + * & the most obvious one is the similarity of the code in the cells. + * + * LCS has no notion of similarity, it only knows about equality. + * We can use other algorithms to find similarity. + * So if we eliminate A, B, we are left with C, D, E, F and we need to find what they map to in `e, F` in modifed document. + * We can use a similarity algorithm to find that. + * + * The purpose of using LCS first is to find the cells that have not changed. + * This avoids the need to use similarity algorithms on all cells. + * + * At the end of the day what we need is as follows + * A <=> A + * B <=> B + * C => Deleted + * D => Deleted + * E => e + * F => F + */ + + + + // Note, if cells are swapped, then this compilicates things + // Trying to solve diff manually is not easy. + // Lets instead use LCS find the cells that haven't changed, + // & the cells that have. + // For the range of cells that have change, lets see if we can get better results using similarity algorithms. + // Assume we have + // Code Cell = print("Hello World") + // Code Cell = print("Foo Bar") + // We now change this to + // MD Cell = # Description + // Code Cell = print("Hello WorldZ") + // Code Cell = print("Foo BarZ") + // LCS will tell us that everything changed. + // But using similarity algorithms we can tell that the first cell is new and last 2 changed. + + + + // Lets try the similarity algorithms on all cells. + // We might fare better. + const result = matchCellBasedOnSimilarties(modified.cells, original.cells); + // If we have at least one match, then great. + if (result.some(c => c.original !== -1)) { + // We have managed to find similarities between cells. + // Now we can definitely find what cell is new/removed. + this.updateCellIdsBasedOnMappings(result, original.cells, modified.cells); + diffUsingCellIds = true; + } + } + + if (!diffUsingCellIds) { + return { + metadataChanged, + cellsDiff: originalDiff + }; + } + + // At this stage we can use internalMetadata.cellId for tracking changes. + // I.e. we compute LCS diff and the hashes of some cells from original will be equal to that in modified as we're using cellId. + // Thus we can find what cells are new/deleted. + // After that we can find whether the contents of the cells changed. + const cellsInsertedOrDeletedDiff = new LcsDiff(CellSequence.createWithCellId(original.cells), CellSequence.createWithCellId(modified.cells)).ComputeDiff(false); + const cellDiffInfo = computeDiff(originalModel, modifiedModel, { cellsDiff: { changes: cellsInsertedOrDeletedDiff.changes, quitEarly: false }, metadataChanged: false, }).cellDiffInfo; + + let processedIndex = 0; + const changes: IDiffChange[] = []; + cellsInsertedOrDeletedDiff.changes.forEach(change => { + if (!change.originalLength && change.modifiedLength) { + // Inserted. + // Find all modified cells before this. + const changeIndex = cellDiffInfo.findIndex(c => c.type === 'insert' && c.modifiedCellIndex === change.modifiedStart); + cellDiffInfo.slice(processedIndex, changeIndex).forEach(c => { + if (c.type === 'unchanged' || c.type === 'modified') { + const originalCell = original.cells[c.originalCellIndex]; + const modifiedCell = modified.cells[c.modifiedCellIndex]; + const changed = c.type === 'modified' || originalCell.getComparisonValue() !== modifiedCell.getComparisonValue(); + if (changed) { + changes.push(new DiffChange(c.originalCellIndex, 1, c.modifiedCellIndex, 1)); + } + } + }); + changes.push(change); + processedIndex = changeIndex + 1; + } else if (change.originalLength && !change.modifiedLength) { + // Deleted. + // Find all modified cells before this. + const changeIndex = cellDiffInfo.findIndex(c => c.type === 'delete' && c.originalCellIndex === change.originalStart); + cellDiffInfo.slice(processedIndex, changeIndex).forEach(c => { + if (c.type === 'unchanged' || c.type === 'modified') { + const originalCell = original.cells[c.originalCellIndex]; + const modifiedCell = modified.cells[c.modifiedCellIndex]; + const changed = c.type === 'modified' || originalCell.getComparisonValue() !== modifiedCell.getComparisonValue(); + if (changed) { + changes.push(new DiffChange(c.originalCellIndex, 1, c.modifiedCellIndex, 1)); + } + } + }); + changes.push(change); + processedIndex = changeIndex + 1; + } else { + // This could be a situation where a cell has been deleted on left and inserted on the right. + // E.g. markdown cell deleted and code cell inserted. + // But LCS shows them as a modification. + const changeIndex = cellDiffInfo.findIndex(c => (c.type === 'delete' && c.originalCellIndex === change.originalStart) || (c.type === 'insert' && c.modifiedCellIndex === change.modifiedStart)); + cellDiffInfo.slice(processedIndex, changeIndex).forEach(c => { + if (c.type === 'unchanged' || c.type === 'modified') { + const originalCell = original.cells[c.originalCellIndex]; + const modifiedCell = modified.cells[c.modifiedCellIndex]; + const changed = c.type === 'modified' || originalCell.getComparisonValue() !== modifiedCell.getComparisonValue(); + if (changed) { + changes.push(new DiffChange(c.originalCellIndex, 1, c.modifiedCellIndex, 1)); + } + } + }); + changes.push(change); + processedIndex = changeIndex + 1; + } + }); + cellDiffInfo.slice(processedIndex).forEach(c => { + if (c.type === 'unchanged' || c.type === 'modified') { + const originalCell = original.cells[c.originalCellIndex]; + const modifiedCell = modified.cells[c.modifiedCellIndex]; + const changed = c.type === 'modified' || originalCell.getComparisonValue() !== modifiedCell.getComparisonValue(); + if (changed) { + changes.push(new DiffChange(c.originalCellIndex, 1, c.modifiedCellIndex, 1)); + } + } + }); + return { - metadataChanged: JSON.stringify(originalMetadata) !== JSON.stringify(modifiedMetadata), - cellsDiff: await this.$computeDiffWithCellIds(original, modified) || diffResult, - // linesDiff: cellLineChanges + metadataChanged, + cellsDiff: { + changes, + quitEarly: false + } }; } - async $computeDiffWithCellIds(original: MirrorNotebookDocument, modified: MirrorNotebookDocument): Promise { + canComputeDiffWithCellIds(original: MirrorNotebookDocument, modified: MirrorNotebookDocument): boolean { + return this.canComputeDiffWithCellInternalIds(original, modified) || this.canComputeDiffWithCellMetadataIds(original, modified); + } + + canComputeDiffWithCellInternalIds(original: MirrorNotebookDocument, modified: MirrorNotebookDocument): boolean { + const originalCellIndexIds = original.cells.map((cell, index) => ({ index, id: (cell.internalMetadata?.cellId || '') as string })); + const modifiedCellIndexIds = modified.cells.map((cell, index) => ({ index, id: (cell.internalMetadata?.cellId || '') as string })); + // If we have a cell without an id, do not use metadata.id for diffing. + if (originalCellIndexIds.some(c => !c.id) || modifiedCellIndexIds.some(c => !c.id)) { + return false; + } + // If none of the ids in original can be found in modified, then we can't use metadata.id for diffing. + // I.e. everything is new, no point trying. + return originalCellIndexIds.some(c => modifiedCellIndexIds.find(m => m.id === c.id)); + } + + canComputeDiffWithCellMetadataIds(original: MirrorNotebookDocument, modified: MirrorNotebookDocument): boolean { const originalCellIndexIds = original.cells.map((cell, index) => ({ index, id: (cell.metadata?.id || '') as string })); const modifiedCellIndexIds = modified.cells.map((cell, index) => ({ index, id: (cell.metadata?.id || '') as string })); - const originalCellIds = originalCellIndexIds.map(c => c.id); - const modifiedCellIds = modifiedCellIndexIds.map(c => c.id); - const orderOrOriginalCellIds = originalCellIds.filter(id => modifiedCellIds.includes(id)).join(','); - const orderOrModifiedCellIds = modifiedCellIds.filter(id => originalCellIds.includes(id)).join(','); - if (originalCellIndexIds.some(c => !c.id) || modifiedCellIndexIds.some(c => !c.id) || orderOrOriginalCellIds !== orderOrModifiedCellIds) { - return; + // If we have a cell without an id, do not use metadata.id for diffing. + if (originalCellIndexIds.some(c => !c.id) || modifiedCellIndexIds.some(c => !c.id)) { + return false; + } + // If none of the ids in original can be found in modified, then we can't use metadata.id for diffing. + // I.e. everything is new, no point trying. + if (originalCellIndexIds.every(c => !modifiedCellIndexIds.find(m => m.id === c.id))) { + return false; } - const diffResult: IDiffResult = { changes: [], quitEarly: false, }; + // Internally we use internalMetadata.cellId for diffing, hence update the internalMetadata.cellId + original.cells.map((cell, index) => { + cell.internalMetadata = cell.internalMetadata || {}; + cell.internalMetadata.cellId = cell.metadata?.id as string || ''; + }); + modified.cells.map((cell, index) => { + cell.internalMetadata = cell.internalMetadata || {}; + cell.internalMetadata.cellId = cell.metadata?.id as string || ''; + }); + return true; + } - const computeCellHashesById = async (notebook: MirrorNotebookDocument) => { - const hashValue = new Map(); - await Promise.all(notebook.cells.map(async (c, i) => { - const value = await c.getComparisonValue(); - // Verified earlier that these cannot be empty. - const id: string = (c.metadata?.id || '') as string; - hashValue.set(id, value); - })); - return hashValue; - }; - const [originalSeq, modifiedSeq] = await Promise.all([computeCellHashesById(original), computeCellHashesById(modified)]); - - while (modifiedCellIndexIds.length) { - const modifiedCell = modifiedCellIndexIds.shift()!; - const originalCell = originalCellIndexIds.find(c => c.id === modifiedCell.id); - if (originalCell) { - // Everything before this cell is a deletion - const index = originalCellIndexIds.indexOf(originalCell); - const deletedFromOriginal = originalCellIndexIds.splice(0, index + 1); - - if (deletedFromOriginal.length === 1) { - if (originalSeq.get(originalCell.id) === modifiedSeq.get(originalCell.id)) { - // Cell contents are the same. - // No changes, hence ignore this cell. - } - else { - diffResult.changes.push({ - originalStart: originalCell.index, - originalLength: 1, - modifiedStart: modifiedCell.index, - modifiedLength: 1 - }); - } - } else { - // This means we have some cells before this and they were removed. - diffResult.changes.push({ - originalStart: deletedFromOriginal[0].index, - originalLength: deletedFromOriginal.length - 1, - modifiedStart: modifiedCell.index, - modifiedLength: 0 - }); - } - continue; + isOriginalCellMatchedWithModifiedCell(originalCell: MirrorCell) { + return (originalCell.internalMetadata?.cellId as string || '').startsWith(PREFIX_FOR_UNMATCHED_ORIGINAL_CELLS); + } + updateCellIdsBasedOnMappings(mappings: { modified: number; original: number }[], originalCells: MirrorCell[], modifiedCells: MirrorCell[]): boolean { + const uuids = new Map(); + originalCells.map((cell, index) => { + cell.internalMetadata = cell.internalMetadata || { cellId: '' }; + cell.internalMetadata.cellId = `${PREFIX_FOR_UNMATCHED_ORIGINAL_CELLS}${generateUuid()}`; + const found = mappings.find(r => r.original === index); + if (found) { + // Do not use the indexes as ids. + // If we do, then the hashes will be very similar except for last digit. + cell.internalMetadata.cellId = generateUuid(); + uuids.set(found.modified, cell.internalMetadata.cellId as string); } - else { - // This is a new cell. - diffResult.changes.push({ - originalStart: originalCellIndexIds.length ? originalCellIndexIds[0].index : original.cells.length, - originalLength: 0, - modifiedStart: modifiedCell.index, - modifiedLength: 1 - }); - } - } - - // If we still have some original cells, then those have been removed. - if (originalCellIndexIds.length) { - diffResult.changes.push({ - originalStart: originalCellIndexIds[0].index, - originalLength: originalCellIndexIds.length, - modifiedStart: modifiedCellIndexIds.length, - modifiedLength: 0 - }); - } - - return diffResult; + }); + modifiedCells.map((cell, index) => { + cell.internalMetadata = cell.internalMetadata || { cellId: '' }; + cell.internalMetadata.cellId = uuids.get(index) ?? generateUuid(); + }); + return true; } $canPromptRecommendation(modelUrl: string): boolean { @@ -368,3 +528,53 @@ export class NotebookEditorSimpleWorker implements IRequestHandler, IDisposable export function create(workerServer: IWorkerServer): IRequestHandler { return new NotebookEditorSimpleWorker(); } + +export type CellDiffInfo = { + originalCellIndex: number; + modifiedCellIndex: number; + type: 'unchanged' | 'modified'; +} | +{ + originalCellIndex: number; + type: 'delete'; +} | +{ + modifiedCellIndex: number; + type: 'insert'; +}; + +interface ICell { + cellKind: CellKind; + getHashValue(): number; + equal(cell: ICell): boolean; +} + +class NotebookTextModelFacade { + public readonly cells: readonly ICell[]; + constructor( + readonly notebook: MirrorNotebookDocument + ) { + + this.cells = notebook.cells.map(cell => new NotebookCellTextModelFacade(cell)); + } + +} +class NotebookCellTextModelFacade implements ICell { + get cellKind(): CellKind { + return this.cell.cellKind; + } + constructor( + private readonly cell: MirrorCell + ) { + } + getHashValue(): number { + return this.cell.getComparisonValue(); + } + equal(cell: ICell): boolean { + if (cell.cellKind !== this.cellKind) { + return false; + } + return this.getHashValue() === cell.getHashValue(); + } + +} diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts index 73ddc03a8d0..1d464006298 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts @@ -71,7 +71,7 @@ suite('notebookCellDiagnostics', () => { extensionPublisherId: '', name: 'testEditorAgent', isDefault: true, - locations: [ChatAgentLocation.Editor], + locations: [ChatAgentLocation.Notebook], metadata: {}, slashCommands: [], disambiguation: [], diff --git a/src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiffService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiffService.test.ts new file mode 100644 index 00000000000..afc0ab1f8ae --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiffService.test.ts @@ -0,0 +1,14750 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { Mimes } from '../../../../../../base/common/mime.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { CellKind, IMainCellDto, IOutputDto, NotebookCellMetadata } from '../../../common/notebookCommon.js'; +import { matchCellBasedOnSimilarties } from '../../../common/services/notebookCellMatching.js'; +import { NotebookEditorSimpleWorker } from '../../../common/services/notebookSimpleWorker.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IDiffChange } from '../../../../../../base/common/diff/diff.js'; + + +suite('NotebookDiff Diff Service', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + let worker: NotebookEditorSimpleWorker; + suiteSetup(() => { + worker = new NotebookEditorSimpleWorker(); + }); + suiteTeardown(() => { + worker.dispose(); + }); + + test('No changes', async () => { + const { mapping, diff } = await mapCells([ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes.length, 0); + }); + + test('diff different source', async () => { + const { mapping, diff } = await mapCells([ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange + ]); + }); + + test('diff different source (2 cells)', async () => { + const { mapping, diff } = await mapCells([ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['z'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange + ]); + }); + + test('diff different source (first cell changed)', async () => { + const { mapping, diff } = await mapCells([ + [['import sys'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['import pandas'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange + ]); + }); + + + test('diff test small source', async () => { + const { mapping, diff } = await mapCells([ + [['123456789'], 'javascript', CellKind.Code, [], {}] + ], [ + [['987654321'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange + ]); + }); + + test('diff test data single cell', async () => { + const { mapping, diff } = await mapCells([ + [[ + '# This version has a bug\n', + 'def mult(a, b):\n', + ' return a / b' + ], 'javascript', CellKind.Code, [], {}] + ], [ + [[ + 'def mult(a, b):\n', + ' \'This version is debugged.\'\n', + ' return a * b' + ], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange + ]); + }); + + test('diff foo/foe (swapped cells)', async () => { + const { mapping, diff } = await mapCells([ + [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { metadata: { collapsed: false }, executionOrder: 5 }], + [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { metadata: { collapsed: false }, executionOrder: 6 }], + [[''], 'javascript', CellKind.Code, [], {}] + ], [ + [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { metadata: { collapsed: false }, executionOrder: 6 }], + [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { metadata: { collapsed: false }, executionOrder: 5 }], + [[''], 'javascript', CellKind.Code, [], {}] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 2 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 1, originalLength: 1, modifiedStart: 2, modifiedLength: 0 } satisfies IDiffChange, + // { originalStart: 1, originalLength: 1, modifiedStart: 2, modifiedLength: 0 } satisfies IDiffChange + ]); + }); + + test('diff foo/foe (swapped cells with changes in output)', async () => { + const { mapping, diff } = await mapCells([ + [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { metadata: { collapsed: false }, executionOrder: 5 }], + [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { metadata: { collapsed: false }, executionOrder: 6 }], + [[''], 'javascript', CellKind.Code, [], {}] + ], [ + [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { metadata: { collapsed: false }, executionOrder: 5 }], + [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { metadata: { collapsed: false }, executionOrder: 6 }], + [[''], 'javascript', CellKind.Code, [], {}] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 2 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 0, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 1, originalLength: 1, modifiedStart: 2, modifiedLength: 0 } satisfies IDiffChange, + ]); + }); + + test('diff markdown', async () => { + const { mapping, diff } = await mapCells([ + [['This is a test notebook with only markdown cells'], 'markdown', CellKind.Markup, [], {}], + [['Lorem ipsum dolor sit amet'], 'markdown', CellKind.Markup, [], {}], + [['In other news'], 'markdown', CellKind.Markup, [], {}], + ], [ + [['This is a test notebook with markdown cells only'], 'markdown', CellKind.Markup, [], {}], + [['Lorem ipsum dolor sit amet'], 'markdown', CellKind.Markup, [], {}], + [['In the news'], 'markdown', CellKind.Markup, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('diff insert 1 at the top', async () => { + const { mapping, diff } = await mapCells([ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}] + ], [ + [['var h = 8;'], 'javascript', CellKind.Code, [], {}], + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('diff insert 1 at the top with lots of cells', async () => { + + const { mapping, diff } = await mapCells([ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ], [ + [['var h = 8;'], 'javascript', CellKind.Code, [], {}], + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 1 }, + { modified: 3, original: 2 }, + { modified: 4, original: 3 }, + { modified: 5, original: 4 }, + { modified: 6, original: 5 }, + { modified: 7, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('diff insert 3 at the top with lots of cells', async () => { + + const { mapping, diff } = await mapCells([ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ], [ + [['var h = 8;'], 'javascript', CellKind.Code, [], {}], + [['var i = 8;'], 'javascript', CellKind.Code, [], {}], + [['var j = 8;'], 'javascript', CellKind.Code, [], {}], + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 }, + { modified: 1, original: -1 }, + { modified: 2, original: -1 }, + { modified: 3, original: 0 }, + { modified: 4, original: 1 }, + { modified: 5, original: 2 }, + { modified: 6, original: 3 }, + { modified: 7, original: 4 }, + { modified: 8, original: 5 }, + { modified: 9, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 3 } satisfies IDiffChange, + ]); + }); + + test('diff insert 1 in the middle', async () => { + + const { mapping, diff } = await mapCells([ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ], [ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var h = 8;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: -1 }, + { modified: 5, original: 4 }, + { modified: 6, original: 5 }, + { modified: 7, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 4, originalLength: 0, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('LCS (swap cells with outputs)', async () => { + const { mapping, diff } = await mapCells([ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }] + ], [ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('LCS (outputs changed)', async () => { + const { mapping, diff } = await mapCells([ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['y'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }] + ], [ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true } }], + [['y'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Swap cells with output and with same code in each cell', async () => { + const { mapping, diff } = await mapCells([ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + [['x = 5'], 'javascript', CellKind.Code, [], {}], + [['x'], 'javascript', CellKind.Code, [], {}], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], {}], + ], [ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['x = 5'], 'javascript', CellKind.Code, [], {}], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], {}], + [['x'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 6, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Change outputs of cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['y'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + [['x = 5'], 'javascript', CellKind.Code, [], {}], + [['x'], 'javascript', CellKind.Code, [], {}], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], {}], + ], [ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['x = 5'], 'javascript', CellKind.Code, [], {}], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], {}], + [['y'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 6, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Insert a new cell', async () => { + const { mapping, diff } = await mapCells([ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ], [ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var h = 8;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: -1 },// + { modified: 5, original: 4 }, + { modified: 6, original: 5 }, + { modified: 7, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 4, originalLength: 0, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('diff output', async () => { + const { mapping, diff } = await mapCells([ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('diff output fast check', async () => { + const { mapping, diff } = await mapCells([ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('No cells', async () => { + const { mapping, diff } = await mapCells([ + ], [ + ]); + + assert.deepStrictEqual(mapping, [ + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + ]); + }); + test('No cells and add 1 cell', async () => { + const { mapping, diff } = await mapCells([ + ], [ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('One cells and delete 1 cell', async () => { + const { mapping, diff } = await mapCells([ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + ]); + + assert.deepStrictEqual(mapping, [ + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 0 } satisfies IDiffChange, + ]); + }); + test('No changes with 1 cell', async () => { + const { mapping, diff } = await mapCells([ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + // { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 0 } satisfies IDiffChange, + ]); + }); + test('One changes with 1 cell', async () => { + const { mapping, diff } = await mapCells([ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], + [ + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('No changes with 2 cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + // { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('One changes with 2 cells, 1st cell changed', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('2 cells, 1st cell deleted', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 0 } satisfies IDiffChange, + ]); + }); + test('2 cells, inserted a cell in the middle', async () => { + const { mapping, diff } = await mapCells([ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined] + ], [ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("Bar Baz")'], 'python', CellKind.Code, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: -1 }, + { modified: 2, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 0, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('One changes with 2 cells, 2nd cell changed', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Two cells, all cells changed', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ] + , [ + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined] + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 2, modifiedStart: 0, modifiedLength: 2 } satisfies IDiffChange, + ]); + }); + test('Insert a md cell and modify the next code cell (few cells)', async () => { + const { mapping, diff } = await mapCells([ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Description'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello worldz")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 0, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Insert a md cell and modify the next code cell', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz1234")'], 'python', CellKind.Code, [], undefined] + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['# New Markdown cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo baz")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz1234")'], 'python', CellKind.Code, [], undefined] + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: -1 }, + { modified: 4, original: 3 }, + { modified: 5, original: 4 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 3, originalLength: 0, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Delete code cell and insert MD cell, both will not match', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['sys.executable'], 'python', CellKind.Code, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['# Header'], 'python', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: -1 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Delete MD cell and insert code cell, both will not match', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['sys.executable'], 'python', CellKind.Code, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['# Header'], 'python', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: -1 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Insert a md cell and modify the next 2 code cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz")'], 'python', CellKind.Code, [], undefined] + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['# New Markdown cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("Foo BaZ")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz Modified")'], 'python', CellKind.Code, [], undefined] + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: -1 }, + { modified: 4, original: 3 }, + { modified: 5, original: 4 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 3, originalLength: 0, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Insert a md cell, delete a cell and modify the next 2 code cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz")'], 'python', CellKind.Code, [], undefined], + [['print("Fox Trot")'], 'python', CellKind.Code, [], undefined] + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['# New Markdown cell'], 'markdown', CellKind.Markup, [], undefined], + // [['print("Foo BaZ")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz Modified")'], 'python', CellKind.Code, [], undefined], + [['print("Fox Trots")'], 'python', CellKind.Code, [], undefined] + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: -1 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Insert a md cell, delete a code cell and modify the next 2 code cells (deleted and inserted lineup)', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz")'], 'python', CellKind.Code, [], undefined], + [['print("Fox Trot")'], 'python', CellKind.Code, [], undefined] + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['sys.executable'], 'python', CellKind.Code, [], undefined], + // [['print("Foo BaZ")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz Modified")'], 'python', CellKind.Code, [], undefined], + [['print("Fox Trot")'], 'python', CellKind.Code, [], undefined] + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Insert a few md cells and modify the next few code cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# New 1'], 'markdown', CellKind.Markup, [], undefined], + [['# New 2'], 'markdown', CellKind.Markup, [], undefined], + [['# Another MD cell (modified1)'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell (modified)'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: -1 }, + { modified: 5, original: -1 }, + { modified: 6, original: 4 }, + { modified: 7, original: 5 }, + { modified: 8, original: 6 }, + { modified: 9, original: 7 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 4, originalLength: 0, modifiedStart: 4, modifiedLength: 2 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 6, originalLength: 1, modifiedStart: 8, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Insert & delete a few md cells and modify the next few code cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# New 1'], 'markdown', CellKind.Markup, [], undefined], + [['# New 2'], 'markdown', CellKind.Markup, [], undefined], + [['# Another MD cell (modified1)'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell (modified)'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: -1 }, + { modified: 5, original: -1 }, + { modified: 6, original: 4 }, + { modified: 7, original: 5 }, + { modified: 8, original: 6 }, + { modified: 9, original: 7 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 4, originalLength: 0, modifiedStart: 4, modifiedLength: 2 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 6, originalLength: 1, modifiedStart: 8, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Modify, then insert ', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo BaZ'], 'markdown', CellKind.Markup, [], undefined], + [['# Header'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: -1 }, + { modified: 4, original: 3 }, + { modified: 5, original: 4 }, + { modified: 6, original: 5 }, + { modified: 7, original: 6 }, + { modified: 8, original: 7 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 0, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 6, originalLength: 1, modifiedStart: 8, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Modify, then delete ', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['sys.executable'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo BaZ'], 'markdown', CellKind.Markup, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['sys.executable'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 4 }, + { modified: 4, original: 5 }, + { modified: 5, original: 6 }, + { modified: 6, original: 7 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 0 } satisfies IDiffChange, + // { originalStart: 6, originalLength: 1, modifiedStart: 8, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Update code cell', async () => { + const { mapping, diff } = await mapCells([ + [ + [ + "# This is a simple notebook\n", + "\n", + "There's nothing special here, I am just writing some text and plotting a function" + ], 'markdown', CellKind.Markup, [], undefined], + [ + [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "\n", + "%matplotlib inline" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "x = np.linspace(0, 4*np.pi,50)\n", + "y = np.sin(x)\n", + "\n", + "plt.plot(x, y)" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "df = pd.DataFrame({f\"column_{c}\": np.random.normal(size=100) for c in range(100)})\n", + "df" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "You could actually do some neat stuff with it, pandas like changing the colors (values above 1 should be green)" + ], 'python', CellKind.Markup, [], undefined], + [ + [ + "df.style.applymap(lambda x: \"background-color: green\" if x>1 else \"background-color: white\")" + ], 'python', CellKind.Code, [], undefined], + [ + [], 'python', CellKind.Code, [], undefined], + ] + , [ + [ + [ + "# This is a simple notebook\n", + "\n", + "There's nothing special here, I am just writing some text and plotting a function" + ], 'markdown', CellKind.Markup, [], undefined], + [ + [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "\n", + "%matplotlib inline" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "x = np.linspace(0, 4*np.pi,50)\n", + "y = 2 * np.sin(x)\n", + "\n", + "plt.plot(x, y)" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "df = pd.DataFrame({f\"column_{c}\": np.random.normal(size=100) for c in range(100)})\n", + "df" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "You could actually do some neat stuff with it, pandas like changing the colors (values above 1 should be green)" + ], 'python', CellKind.Markup, [], undefined], + [ + [ + "df.style.applymap(lambda x: \"background-color: green\" if x>1 else \"background-color: white\")" + ], 'python', CellKind.Code, [], undefined], + [ + [], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + ]); + + }); + test('Update code cell 1', async () => { + const { mapping, diff } = await mapCells([ + [ + [ + "# Hello World" + ], 'markdown', CellKind.Markup, [], undefined], + [ + [ + "import os\n", + "from pprint import pprint \n", + "from sklearn.preprocessing import LabelEncoder\n", + "import ast\n", + "import pandas as pd\n", + "from clipper import clipper\n", + "import ffmpeg" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "data_dir = '../../../app/data/'\n", + "output_dir = 'output/'\n", + "video_dir = 'videos/'\n", + "\n", + "file_path = data_dir+output_dir+data_file\n", + "video_path = data_dir+video_dir+video_file\n", + "\n", + "df = pd.read_csv(file_path)\n", + "df.head()" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "# convert the string representation of results to a list\n", + "df['list_categories'] = df['categories'].apply(ast.literal_eval)\n", + "\n", + "# get most likely label from each list\n", + "df['label'] = df['list_categories'].apply(lambda x: x[0] if x else None)" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "# initialize the LabelEncoder\n", + "label_encoder = LabelEncoder()\n", + "\n", + "# fit and transform the label to a numerical value\n", + "numerical_data = label_encoder.fit_transform(df['label'])\n", + "\n", + "df['label_value'] = numerical_data" + ], 'python', CellKind.Markup, [], undefined], + ] + , [ + [ + [ + "# Updated markdown cell" + ], 'markdown', CellKind.Markup, [], undefined], + [ + [ + "from sklearn.preprocessing import LabelEncoder\n", + "import ast\n", + "import pandas as pd\n", + "from clipper import clipper\n", + "import ffmpeg" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "data_dir = '../../../app/data/'\n", + "output_dir = 'output/'\n", + "video_dir = 'videos/'\n", + "\n", + "file_path = data_dir+output_dir+data_file\n", + "video_path = data_dir+video_dir+video_file\n", + "\n", + "df = pd.read_csv(file_path)\n", + "df.head()" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "# convert the string representation of results to a list\n", + "df['list_categories'] = df['categories'].apply(ast.literal_eval)\n", + "\n", + "# get most likely label from each list\n", + "df['label'] = df['list_categories'].apply(lambda x: x[0] if x else None)" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "# initialize the LabelEncoder\n", + "label_encoder = LabelEncoder()\n", + "\n", + "# fit and transform the label to a numerical value\n", + "numerical_data = label_encoder.fit_transform(df['label'])\n", + "\n", + "df['label_value'] = numerical_data" + ], 'python', CellKind.Markup, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Detect modification and insertion of cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "Make sure your [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) is working properly.\n", + "To avoid errors that may arise from the version of the dependency package, it is recommended to use the **JupyterLab** instead of the Jupyter notebook to display image results.\n", + "```\n", + "- pip install --upgrade pip && pip install -r requirements.txt\n", + "- jupyter labextension install --no-build @jupyter-widgets/jupyterlab-manager\n", + "- jupyter labextension install --no-build jupyter-datawidgets/extension\n", + "- jupyter labextension install jupyter-threejs\n", + "- jupyter labextension list\n", + "```\n", + "\n", + "You should see:\n", + "```\n", + "JupyterLab v...\n", + " ...\n", + " jupyterlab-datawidgets v... enabled OK\n", + " @jupyter-widgets/jupyterlab-manager v... enabled OK\n", + " jupyter-threejs v... enabled OK\n", + "```\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using a Jupyter Notebook release. Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino-dev>=2024.0.0\" \"opencv-python\" \"torch\" \"onnx<1.16.2\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "with open(\"notebook_utils.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/engine3js.py\",\n", + ")\n", + "with open(\"engine3js.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "import notebook_utils as utils\n", + "import engine3js as engine" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We use `omz_downloader`, which is a command line tool from the `openvino-dev` package. `omz_downloader` automatically creates a directory structure and downloads the selected model." + ] + }, + { + "cell_type": "code", + "source": [ + "# directory where model will be downloaded\n", + "base_model_dir = \"model\"\n", + "\n", + "# model name as named in Open Model Zoo\n", + "model_name = \"human-pose-estimation-3d-0001\"\n", + "# selected precision (FP32, FP16)\n", + "precision = \"FP32\"\n", + "\n", + "BASE_MODEL_NAME = f\"{base_model_dir}/public/{model_name}/{model_name}\"\n", + "model_path = Path(BASE_MODEL_NAME).with_suffix(\".pth\")\n", + "onnx_path = Path(BASE_MODEL_NAME).with_suffix(\".onnx\")\n", + "\n", + "ir_model_path = Path(f\"model/public/{model_name}/{precision}/{model_name}.xml\")\n", + "model_weights_path = Path(f\"model/public/{model_name}/{precision}/{model_name}.bin\")\n", + "\n", + "if not model_path.exists():\n", + " download_command = f\"omz_downloader \" f\"--name {model_name} \" f\"--output_dir {base_model_dir}\"\n", + " ! $download_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The selected model comes from the public directory, which means it must be converted into OpenVINO Intermediate Representation (OpenVINO IR). We use `omz_converter` to convert the ONNX format model to the OpenVINO IR format." + ] + }, + { + "cell_type": "code", + "source": [ + "if not onnx_path.exists():\n", + " convert_command = (\n", + " f\"omz_converter \" f\"--name {model_name} \" f\"--precisions {precision} \" f\"--download_dir {base_model_dir} \" f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $convert_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "device = utils.device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(model=ir_model_path, weights=model_weights_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)\n", + "infer_request = compiled_model.create_infer_request()\n", + "input_tensor_name = model.inputs[0].get_any_name()\n", + "\n", + "# get input and output names of nodes\n", + "input_layer = compiled_model.input(0)\n", + "output_layers = list(compiled_model.outputs)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The input for the model is data from the input image and the outputs are heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "input_layer.any_name, [o.any_name for o in output_layers]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[None,]\n", + " infer_request.infer({input_tensor_name: img})\n", + " # A set of three inference results is obtained\n", + " results = {name: infer_request.get_tensor(name).data[:] for name in {\"features\", \"heatmaps\", \"pafs\"}}\n", + " # Get the results\n", + " results = (results[\"features\"][0], results[\"heatmaps\"][0], results[\"pafs\"][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1],\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15],\n", + " [15, 16], # nose - l_eye - l_ear\n", + " [1, 17],\n", + " [17, 18], # nose - r_eye - r_ear\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16],\n", + " [16, 18], # nose - l_eye - l_ear\n", + " [1, 15],\n", + " [15, 17], # nose - r_eye - r_ear\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(source, flip=flip, fps=30, skip_first_frames=skip_frames)\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(grid=True, axis=True, view_width=windows_width, view_height=windows_height)\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(format=\"jpg\", height=windows_height, width=windows_width)\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2]))\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = engine.parse_poses(inference_result, 1, stride, focal_length, True)\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://storage.openvinotoolkit.org/data/test_data/videos/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + , [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "Make sure your [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) is working properly.\n", + "To avoid errors that may arise from the version of the dependency package, it is recommended to use the **JupyterLab** instead of the Jupyter notebook to display image results.\n", + "```\n", + "- pip install --upgrade pip && pip install -r requirements.txt\n", + "- jupyter labextension install --no-build @jupyter-widgets/jupyterlab-manager\n", + "- jupyter labextension install --no-build jupyter-datawidgets/extension\n", + "- jupyter labextension install jupyter-threejs\n", + "- jupyter labextension list\n", + "```\n", + "\n", + "You should see:\n", + "```\n", + "JupyterLab v...\n", + " ...\n", + " jupyterlab-datawidgets v... enabled OK\n", + " @jupyter-widgets/jupyterlab-manager v... enabled OK\n", + " jupyter-threejs v... enabled OK\n", + "```\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using a Jupyter Notebook release. Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino>=2024.4.0\" \"opencv-python\" \"torch\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "with open(\"notebook_utils.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/engine3js.py\",\n", + ")\n", + "with open(\"engine3js.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "import notebook_utils as utils\n", + "import engine3js as engine" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "source": [ + "from notebook_utils import download_file\n", + "import tarfile\n", + "\n", + "\n", + "# directory where model will be downloaded\n", + "base_model_dir = Path(\"model\")\n", + "\n", + "download_file(\"https://storage.openvinotoolkit.org/repositories/open_model_zoo/public/2022.1/human-pose-estimation-3d-0001/human-pose-estimation-3d.tar.gz\", directory=base_model_dir)\n", + "\n", + "ckpt_file = base_model_dir / \"human-pose-estimation-3d-0001.pth\"\n", + "\n", + "if not ckpt_file.exists():\n", + " with tarfile.open(base_model_dir / \"human-pose-estimation-3d.tar.gz\") as f:\n", + " f.extractall(base_model_dir)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "source": [ + "import torch\n", + "import openvino as ov\n", + "\n", + "ov_model_path = Path(base_model_dir) / \"human-pose-estimation-3d-0001.xml\"\n", + "\n", + "if not ov_model_path.exists():\n", + " from model.model import PoseEstimationWithMobileNet\n", + "\n", + " pose_estimation_model = PoseEstimationWithMobileNet(is_convertible_by_mo=True)\n", + " pose_estimation_model.loas_state_dict(torch.load(ckpt_file, map_location=\"cpu\"))\n", + " pose_estimation_model.eval()\n", + "\n", + " with torch.no_grad():\n", + " ov_model = ov.convert_model(pose_estimation_model, example_input=torch.zeros([1, 3, 256, 448]), input=[1, 3, 256, 448])\n", + " ov.save_model(ov_model, ov_model_path)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "device = utils.device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(ov_model_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " mean_value = 128.0\n", + " scale_value = 255.0\n", + "\n", + " img = (img - mean_value) / scale_value\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[None,]\n", + " result = compiled_model(img)\n", + " # Get the results\n", + " results = (result[0][0], results[1][0], results[2][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1],\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15],\n", + " [15, 16], # nose - l_eye - l_ear\n", + " [1, 17],\n", + " [17, 18], # nose - r_eye - r_ear\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16],\n", + " [16, 18], # nose - l_eye - l_ear\n", + " [1, 15],\n", + " [15, 17], # nose - r_eye - r_ear\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(source, flip=flip, fps=30, skip_first_frames=skip_frames)\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(grid=True, axis=True, view_width=windows_width, view_height=windows_height)\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(format=\"jpg\", height=windows_height, width=windows_width)\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2]))\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = engine.parse_poses(inference_result, 1, stride, focal_length, True)\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://storage.openvinotoolkit.org/data/test_data/videos/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 15 }, + { modified: 14, original: 16 }, + { modified: 15, original: 17 }, + { modified: 16, original: 18 }, + { modified: 17, original: 19 }, + { modified: 18, original: 20 }, + { modified: 19, original: 21 }, + { modified: 20, original: 22 }, + { modified: 21, original: 23 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + { + modifiedLength: 1, + modifiedStart: 6, + originalLength: 1, + originalStart: 6 + }, + { + modifiedLength: 1, + modifiedStart: 7, + originalLength: 1, + originalStart: 7 + }, + { + modifiedLength: 1, + modifiedStart: 8, + originalLength: 1, + originalStart: 8 + }, + { + modifiedLength: 1, + modifiedStart: 12, + originalLength: 1, + originalStart: 12 + }, + { + modifiedLength: 0, + modifiedStart: 13, + originalLength: 2, + originalStart: 13 + }, + { + modifiedLength: 1, + modifiedStart: 14, + originalLength: 1, + originalStart: 16 + } + ]); + }); + test('Detect modification in 2 cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using the latest Jupyter Notebook release (2.4.1). Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino-dev>=2024.0.0\" \"opencv-python\" \"torch\" \"onnx\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import sys\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "import notebook_utils as utils\n", + "\n", + "sys.path.append(\"./engine\")\n", + "import engine.engine3js as engine\n", + "from engine.parse_poses import parse_poses" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We use `omz_downloader`, which is a command line tool from the `openvino-dev` package. `omz_downloader` automatically creates a directory structure and downloads the selected model." + ] + }, + { + "cell_type": "code", + "source": [ + "# directory where model will be downloaded\n", + "base_model_dir = \"model\"\n", + "\n", + "# model name as named in Open Model Zoo\n", + "model_name = \"human-pose-estimation-3d-0001\"\n", + "# selected precision (FP32, FP16)\n", + "precision = \"FP32\"\n", + "\n", + "BASE_MODEL_NAME = f\"{base_model_dir}/public/{model_name}/{model_name}\"\n", + "model_path = Path(BASE_MODEL_NAME).with_suffix(\".pth\")\n", + "onnx_path = Path(BASE_MODEL_NAME).with_suffix(\".onnx\")\n", + "\n", + "ir_model_path = f\"model/public/{model_name}/{precision}/{model_name}.xml\"\n", + "model_weights_path = f\"model/public/{model_name}/{precision}/{model_name}.bin\"\n", + "\n", + "if not model_path.exists():\n", + " download_command = f\"omz_downloader \" f\"--name {model_name} \" f\"--output_dir {base_model_dir}\"\n", + " ! $download_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The selected model comes from the public directory, which means it must be converted into OpenVINO Intermediate Representation (OpenVINO IR). We use `omz_converter` to convert the ONNX format model to the OpenVINO IR format." + ] + }, + { + "cell_type": "code", + "source": [ + "if not onnx_path.exists():\n", + " convert_command = (\n", + " f\"omz_converter \" f\"--name {model_name} \" f\"--precisions {precision} \" f\"--download_dir {base_model_dir} \" f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $convert_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()\n", + "\n", + "device = widgets.Dropdown(\n", + " options=core.available_devices + [\"AUTO\"],\n", + " value=\"AUTO\",\n", + " description=\"Device:\",\n", + " disabled=False,\n", + ")\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(model=ir_model_path, weights=model_weights_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)\n", + "infer_request = compiled_model.create_infer_request()\n", + "input_tensor_name = model.inputs[0].get_any_name()\n", + "\n", + "# get input and output names of nodes\n", + "input_layer = compiled_model.input(0)\n", + "output_layers = list(compiled_model.outputs)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The input for the model is data from the input image and the outputs are heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "input_layer.any_name, [o.any_name for o in output_layers]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[None,]\n", + " infer_request.infer({input_tensor_name: img})\n", + " # A set of three inference results is obtained\n", + " results = {name: infer_request.get_tensor(name).data[:] for name in {\"features\", \"heatmaps\", \"pafs\"}}\n", + " # Get the results\n", + " results = (results[\"features\"][0], results[\"heatmaps\"][0], results[\"pafs\"][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1],\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15],\n", + " [15, 16], # nose - l_eye - l_ear\n", + " [1, 17],\n", + " [17, 18], # nose - r_eye - r_ear\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16],\n", + " [16, 18], # nose - l_eye - l_ear\n", + " [1, 15],\n", + " [15, 17], # nose - r_eye - r_ear\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(source, flip=flip, fps=30, skip_first_frames=skip_frames)\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(grid=True, axis=True, view_width=windows_width, view_height=windows_height)\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(format=\"jpg\", height=windows_height, width=windows_width)\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2]))\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = parse_poses(inference_result, 1, stride, focal_length, True)\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://github.com/intel-iot-devkit/sample-videos/raw/master/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + , [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using a Jupyter Notebook release. Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino-dev>=2024.0.0\" \"opencv-python\" \"torch\" \"onnx\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "with open(\"notebook_utils.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/engine3js.py\",\n", + ")\n", + "with open(\"engine3js.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "import notebook_utils as utils\n", + "import engine3js as engine" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We use `omz_downloader`, which is a command line tool from the `openvino-dev` package. `omz_downloader` automatically creates a directory structure and downloads the selected model." + ] + }, + { + "cell_type": "code", + "source": [ + "# directory where model will be downloaded\n", + "base_model_dir = \"model\"\n", + "\n", + "# model name as named in Open Model Zoo\n", + "model_name = \"human-pose-estimation-3d-0001\"\n", + "# selected precision (FP32, FP16)\n", + "precision = \"FP32\"\n", + "\n", + "BASE_MODEL_NAME = f\"{base_model_dir}/public/{model_name}/{model_name}\"\n", + "model_path = Path(BASE_MODEL_NAME).with_suffix(\".pth\")\n", + "onnx_path = Path(BASE_MODEL_NAME).with_suffix(\".onnx\")\n", + "\n", + "ir_model_path = f\"model/public/{model_name}/{precision}/{model_name}.xml\"\n", + "model_weights_path = f\"model/public/{model_name}/{precision}/{model_name}.bin\"\n", + "\n", + "if not model_path.exists():\n", + " download_command = f\"omz_downloader \" f\"--name {model_name} \" f\"--output_dir {base_model_dir}\"\n", + " ! $download_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The selected model comes from the public directory, which means it must be converted into OpenVINO Intermediate Representation (OpenVINO IR). We use `omz_converter` to convert the ONNX format model to the OpenVINO IR format." + ] + }, + { + "cell_type": "code", + "source": [ + "if not onnx_path.exists():\n", + " convert_command = (\n", + " f\"omz_converter \" f\"--name {model_name} \" f\"--precisions {precision} \" f\"--download_dir {base_model_dir} \" f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $convert_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()\n", + "\n", + "device = widgets.Dropdown(\n", + " options=core.available_devices + [\"AUTO\"],\n", + " value=\"AUTO\",\n", + " description=\"Device:\",\n", + " disabled=False,\n", + ")\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(model=ir_model_path, weights=model_weights_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)\n", + "infer_request = compiled_model.create_infer_request()\n", + "input_tensor_name = model.inputs[0].get_any_name()\n", + "\n", + "# get input and output names of nodes\n", + "input_layer = compiled_model.input(0)\n", + "output_layers = list(compiled_model.outputs)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The input for the model is data from the input image and the outputs are heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "input_layer.any_name, [o.any_name for o in output_layers]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[None,]\n", + " infer_request.infer({input_tensor_name: img})\n", + " # A set of three inference results is obtained\n", + " results = {name: infer_request.get_tensor(name).data[:] for name in {\"features\", \"heatmaps\", \"pafs\"}}\n", + " # Get the results\n", + " results = (results[\"features\"][0], results[\"heatmaps\"][0], results[\"pafs\"][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1],\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15],\n", + " [15, 16], # nose - l_eye - l_ear\n", + " [1, 17],\n", + " [17, 18], # nose - r_eye - r_ear\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16],\n", + " [16, 18], # nose - l_eye - l_ear\n", + " [1, 15],\n", + " [15, 17], # nose - r_eye - r_ear\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(source, flip=flip, fps=30, skip_first_frames=skip_frames)\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(grid=True, axis=True, view_width=windows_width, view_height=windows_height)\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(format=\"jpg\", height=windows_height, width=windows_width)\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2]))\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = engine.parse_poses(inference_result, 1, stride, focal_length, True)\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://github.com/intel-iot-devkit/sample-videos/raw/master/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 20, originalLength: 1, modifiedStart: 20, modifiedLength: 1 } satisfies IDiffChange, + ]); + + }); + test('Detect modification in multiple cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using the latest Jupyter Notebook release (2.4.1). Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino-dev>=2024.0.0\" \"opencv-python\" \"torch\" \"onnx\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import sys\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "r = requests.get(\n", + " url='https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py',\n", + ")\n", + "open('notebook_utils.py', 'w').write(r.text)\n", + "import notebook_utils as utils\n", + "\n", + "sys.path.append(\"./engine\")\n", + "import engine.engine3js as engine\n", + "from engine.parse_poses import parse_poses" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We use `omz_downloader`, which is a command line tool from the `openvino-dev` package. `omz_downloader` automatically creates a directory structure and downloads the selected model." + ] + }, + { + "cell_type": "code", + "source": [ + "# directory where model will be downloaded\n", + "base_model_dir = \"model\"\n", + "\n", + "# model name as named in Open Model Zoo\n", + "model_name = \"human-pose-estimation-3d-0001\"\n", + "# selected precision (FP32, FP16)\n", + "precision = \"FP32\"\n", + "\n", + "BASE_MODEL_NAME = f\"{base_model_dir}/public/{model_name}/{model_name}\"\n", + "model_path = Path(BASE_MODEL_NAME).with_suffix(\".pth\")\n", + "onnx_path = Path(BASE_MODEL_NAME).with_suffix(\".onnx\")\n", + "\n", + "ir_model_path = f\"model/public/{model_name}/{precision}/{model_name}.xml\"\n", + "model_weights_path = f\"model/public/{model_name}/{precision}/{model_name}.bin\"\n", + "\n", + "if not model_path.exists():\n", + " download_command = (\n", + " f\"omz_downloader \" f\"--name {model_name} \" f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $download_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The selected model comes from the public directory, which means it must be converted into OpenVINO Intermediate Representation (OpenVINO IR). We use `omz_converter` to convert the ONNX format model to the OpenVINO IR format." + ] + }, + { + "cell_type": "code", + "source": [ + "if not onnx_path.exists():\n", + " convert_command = (\n", + " f\"omz_converter \"\n", + " f\"--name {model_name} \"\n", + " f\"--precisions {precision} \"\n", + " f\"--download_dir {base_model_dir} \"\n", + " f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $convert_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()\n", + "\n", + "device = widgets.Dropdown(\n", + " options=core.available_devices + [\"AUTO\"],\n", + " value='AUTO',\n", + " description='Device:',\n", + " disabled=False,\n", + ")\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(model=ir_model_path, weights=model_weights_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)\n", + "infer_request = compiled_model.create_infer_request()\n", + "input_tensor_name = model.inputs[0].get_any_name()\n", + "\n", + "# get input and output names of nodes\n", + "input_layer = compiled_model.input(0)\n", + "output_layers = list(compiled_model.outputs)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The input for the model is data from the input image and the outputs are heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "input_layer.any_name, [o.any_name for o in output_layers]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[\n", + " None,\n", + " ]\n", + " infer_request.infer({input_tensor_name: img})\n", + " # A set of three inference results is obtained\n", + " results = {\n", + " name: infer_request.get_tensor(name).data[:]\n", + " for name in {\"features\", \"heatmaps\", \"pafs\"}\n", + " }\n", + " # Get the results\n", + " results = (results[\"features\"][0], results[\"heatmaps\"][0], results[\"pafs\"][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1], \n", + " [0, 9], [9, 10], [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3], [3, 4], [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15], [15, 16], # nose - l_eye - l_ear\n", + " [1, 17], [17, 18], # nose - r_eye - r_ear\n", + " [0, 6], [6, 7], [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12], [12, 13], [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16], [16, 18], # nose - l_eye - l_ear\n", + " [1, 15], [15, 17], # nose - r_eye - r_ear\n", + " [0, 3], [3, 4], [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9], [9, 10], [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6], [6, 7], [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12], [12, 13], [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ") \n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(source, flip=flip, fps=30, skip_first_frames=skip_frames)\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(grid=True, axis=True, view_width=windows_width, view_height=windows_height)\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(\n", + " format=\"jpg\", height=windows_height, width=windows_width\n", + " )\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2]))\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = parse_poses(inference_result, 1, stride, focal_length, True)\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://github.com/intel-iot-devkit/sample-videos/raw/master/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + , [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using the latest Jupyter Notebook release (2.4.1). Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino-dev>=2024.0.0\" \"opencv-python\" \"torch\" \"onnx\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import sys\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "import notebook_utils as utils\n", + "\n", + "sys.path.append(\"./engine\")\n", + "import engine.engine3js as engine\n", + "from engine.parse_poses import parse_poses" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We use `omz_downloader`, which is a command line tool from the `openvino-dev` package. `omz_downloader` automatically creates a directory structure and downloads the selected model." + ] + }, + { + "cell_type": "code", + "source": [ + "# directory where model will be downloaded\n", + "base_model_dir = \"model\"\n", + "\n", + "# model name as named in Open Model Zoo\n", + "model_name = \"human-pose-estimation-3d-0001\"\n", + "# selected precision (FP32, FP16)\n", + "precision = \"FP32\"\n", + "\n", + "BASE_MODEL_NAME = f\"{base_model_dir}/public/{model_name}/{model_name}\"\n", + "model_path = Path(BASE_MODEL_NAME).with_suffix(\".pth\")\n", + "onnx_path = Path(BASE_MODEL_NAME).with_suffix(\".onnx\")\n", + "\n", + "ir_model_path = f\"model/public/{model_name}/{precision}/{model_name}.xml\"\n", + "model_weights_path = f\"model/public/{model_name}/{precision}/{model_name}.bin\"\n", + "\n", + "if not model_path.exists():\n", + " download_command = (\n", + " f\"omz_downloader \" f\"--name {model_name} \" f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $download_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The selected model comes from the public directory, which means it must be converted into OpenVINO Intermediate Representation (OpenVINO IR). We use `omz_converter` to convert the ONNX format model to the OpenVINO IR format." + ] + }, + { + "cell_type": "code", + "source": [ + "if not onnx_path.exists():\n", + " convert_command = (\n", + " f\"omz_converter \"\n", + " f\"--name {model_name} \"\n", + " f\"--precisions {precision} \"\n", + " f\"--download_dir {base_model_dir} \"\n", + " f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $convert_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()\n", + "\n", + "device = widgets.Dropdown(\n", + " options=core.available_devices + [\"AUTO\"],\n", + " value=\"AUTO\",\n", + " description=\"Device:\",\n", + " disabled=False,\n", + ")\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(model=ir_model_path, weights=model_weights_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)\n", + "infer_request = compiled_model.create_infer_request()\n", + "input_tensor_name = model.inputs[0].get_any_name()\n", + "\n", + "# get input and output names of nodes\n", + "input_layer = compiled_model.input(0)\n", + "output_layers = list(compiled_model.outputs)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The input for the model is data from the input image and the outputs are heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "input_layer.any_name, [o.any_name for o in output_layers]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[None,]\n", + " infer_request.infer({input_tensor_name: img})\n", + " # A set of three inference results is obtained\n", + " results = {\n", + " name: infer_request.get_tensor(name).data[:]\n", + " for name in {\"features\", \"heatmaps\", \"pafs\"}\n", + " }\n", + " # Get the results\n", + " results = (results[\"features\"][0], results[\"heatmaps\"][0], results[\"pafs\"][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1],\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15],\n", + " [15, 16], # nose - l_eye - l_ear\n", + " [1, 17],\n", + " [17, 18], # nose - r_eye - r_ear\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16],\n", + " [16, 18], # nose - l_eye - l_ear\n", + " [1, 15],\n", + " [15, 17], # nose - r_eye - r_ear\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(\n", + " source, flip=flip, fps=30, skip_first_frames=skip_frames\n", + " )\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(\n", + " grid=True, axis=True, view_width=windows_width, view_height=windows_height\n", + " )\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(\n", + " format=\"jpg\", height=windows_height, width=windows_width\n", + " )\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(\n", + " frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2])\n", + " )\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = parse_poses(\n", + " inference_result, 1, stride, focal_length, True\n", + " )\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://github.com/intel-iot-devkit/sample-videos/raw/master/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 4, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 10, originalLength: 1, modifiedStart: 10, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 16, originalLength: 1, modifiedStart: 16, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 18, originalLength: 1, modifiedStart: 18, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 20, originalLength: 1, modifiedStart: 20, modifiedLength: 1 } satisfies IDiffChange, + ]); + + }); + test('Modification with Insertion, detected as deletion of a cell', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "source": [ + "# Video generation with ZeroScope and OpenVINO\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Install and import required packages](#Install-and-import-required-packages)\n", + "- [Load the model](#Load-the-model)\n", + "- [Convert the model](#Convert-the-model)\n", + " - [Define the conversion function](#Define-the-conversion-function)\n", + " - [UNet](#UNet)\n", + " - [VAE](#VAE)\n", + " - [Text encoder](#Text-encoder)\n", + "- [Build a pipeline](#Build-a-pipeline)\n", + "- [Inference with OpenVINO](#Inference-with-OpenVINO)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Define a prompt](#Define-a-prompt)\n", + " - [Video generation](#Video-generation)\n", + "- [Interactive demo](#Interactive-demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The ZeroScope model is a free and open-source text-to-video model that can generate realistic and engaging videos from text descriptions. It is based on the [Modelscope](https://modelscope.cn/models/damo/text-to-video-synthesis/summary) model, but it has been improved to produce higher-quality videos with a 16:9 aspect ratio and no Shutterstock watermark. The ZeroScope model is available in two versions: ZeroScope_v2 576w, which is optimized for rapid content creation at a resolution of 576x320 pixels, and ZeroScope_v2 XL, which upscales videos to a high-definition resolution of 1024x576.\n", + "\n", + "The ZeroScope model is trained on a dataset of over 9,000 videos and 29,000 tagged frames. It uses a diffusion model to generate videos, which means that it starts with a random noise image and gradually adds detail to it until it matches the text description. The ZeroScope model is still under development, but it has already been used to create some impressive videos. For example, it has been used to create videos of people dancing, playing sports, and even driving cars.\n", + "\n", + "The ZeroScope model is a powerful tool that can be used to create various videos, from simple animations to complex scenes. It is still under development, but it has the potential to revolutionize the way we create and consume video content.\n", + "\n", + "Both versions of the ZeroScope model are available on Hugging Face:\n", + " - [ZeroScope_v2 576w](https://huggingface.co/cerspense/zeroscope_v2_576w)\n", + " - [ZeroScope_v2 XL](https://huggingface.co/cerspense/zeroscope_v2_XL)\n", + "\n", + "We will use the first one." + ] + }, + { + "cell_type": "markdown", + "source": [ + "
\n", + " This tutorial requires at least 24GB of free memory to generate a video with a frame size of 432x240 and 16 frames. Increasing either of these values will require more memory and take more time.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Install and import required packages\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "To work with text-to-video synthesis model, we will use Hugging Face's [Diffusers](https://github.com/huggingface/diffusers) library. It provides already pretrained model from `cerspense`." + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install -q --extra-index-url https://download.pytorch.org/whl/cpu \"diffusers>=0.18.0\" \"torch>=2.1\" transformers \"openvino>=2023.1.0\" numpy \"gradio>=4.19\"" + ] + }, + { + "cell_type": "code", + "source": [ + "import gc\n", + "from typing import Optional, Union, List, Callable\n", + "import base64\n", + "import tempfile\n", + "import warnings\n", + "\n", + "import diffusers\n", + "import transformers\n", + "import numpy as np\n", + "import IPython\n", + "import torch\n", + "import PIL\n", + "import gradio as gr\n", + "\n", + "import openvino as ov" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Original 576x320 inference requires a lot of RAM (>100GB), so let's run our example on a smaller frame size, keeping the same aspect ratio. Try reducing values below to reduce the memory consumption." + ] + }, + { + "cell_type": "code", + "source": [ + "WIDTH = 432 # must be divisible by 8\n", + "HEIGHT = 240 # must be divisible by 8\n", + "NUM_FRAMES = 16" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The model is loaded from HuggingFace using `.from_pretrained` method of `diffusers.DiffusionPipeline`." + ] + }, + { + "cell_type": "code", + "source": [ + "pipe = diffusers.DiffusionPipeline.from_pretrained(\"cerspense/zeroscope_v2_576w\")" + ] + }, + { + "cell_type": "code", + "source": [ + "unet = pipe.unet\n", + "unet.eval()\n", + "vae = pipe.vae\n", + "vae.eval()\n", + "text_encoder = pipe.text_encoder\n", + "text_encoder.eval()\n", + "tokenizer = pipe.tokenizer\n", + "scheduler = pipe.scheduler\n", + "vae_scale_factor = pipe.vae_scale_factor\n", + "unet_in_channels = pipe.unet.config.in_channels\n", + "sample_width = WIDTH // vae_scale_factor\n", + "sample_height = HEIGHT // vae_scale_factor\n", + "del pipe\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Convert the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The architecture for generating videos from text comprises three distinct sub-networks: one for extracting text features, another for translating text features into the video latent space using a diffusion model, and a final one for mapping the video latent space to the visual space. The collective parameters of the entire model amount to approximately 1.7 billion. It's capable of processing English input. The diffusion model is built upon the Unet3D model and achieves video generation by iteratively denoising a starting point of pure Gaussian noise video." + ] + }, + { + "cell_type": "markdown", + "source": [] + }, + { + "cell_type": "markdown", + "source": [ + "### Define the conversion function\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Model components are PyTorch modules, that can be converted with `ov.convert_model` function directly. We also use `ov.save_model` function to serialize the result of conversion." + ] + }, + { + "cell_type": "code", + "source": [ + "warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)" + ] + }, + { + "cell_type": "code", + "source": [ + "from pathlib import Path\n", + "\n", + "\n", + "def convert(model: torch.nn.Module, xml_path: str, **convert_kwargs) -> Path:\n", + " xml_path = Path(xml_path)\n", + " if not xml_path.exists():\n", + " xml_path.parent.mkdir(parents=True, exist_ok=True)\n", + " with torch.no_grad():\n", + " converted_model = ov.convert_model(model, **convert_kwargs)\n", + " ov.save_model(converted_model, xml_path)\n", + " del converted_model\n", + " gc.collect()\n", + " torch._C._jit_clear_class_registry()\n", + " torch.jit._recursive.concrete_type_store = torch.jit._recursive.ConcreteTypeStore()\n", + " torch.jit._state._clear_class_state()\n", + " return xml_path" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### UNet\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text-to-video generation pipeline main component is a conditional 3D UNet model that takes a noisy sample, conditional state, and a timestep and returns a sample shaped output." + ] + }, + { + "cell_type": "code", + "source": [ + "unet_xml_path = convert(\n", + " unet,\n", + " \"models/unet.xml\",\n", + " example_input={\n", + " \"sample\": torch.randn(2, 4, 2, int(sample_height // 2), int(sample_width // 2)),\n", + " \"timestep\": torch.tensor(1),\n", + " \"encoder_hidden_states\": torch.randn(2, 77, 1024),\n", + " },\n", + " input=[\n", + " (\"sample\", (2, 4, NUM_FRAMES, sample_height, sample_width)),\n", + " (\"timestep\", ()),\n", + " (\"encoder_hidden_states\", (2, 77, 1024)),\n", + " ],\n", + ")\n", + "del unet\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### VAE\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Variational autoencoder (VAE) uses UNet output to decode latents to visual representations. Our VAE model has KL loss for encoding images into latents and decoding latent representations into images. For inference, we need only decoder part." + ] + }, + { + "cell_type": "code", + "source": [ + "class VaeDecoderWrapper(torch.nn.Module):\n", + " def __init__(self, vae):\n", + " super().__init__()\n", + " self.vae = vae\n", + "\n", + " def forward(self, z: torch.FloatTensor):\n", + " return self.vae.decode(z)" + ] + }, + { + "cell_type": "code", + "source": [ + "vae_decoder_xml_path = convert(\n", + " VaeDecoderWrapper(vae),\n", + " \"models/vae.xml\",\n", + " example_input=torch.randn(2, 4, 32, 32),\n", + " input=((NUM_FRAMES, 4, sample_height, sample_width)),\n", + ")\n", + "del vae\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Text encoder\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text encoder is used to encode the input prompt to tensor. Default tensor length is 77." + ] + }, + { + "cell_type": "code", + "source": [ + "text_encoder_xml = convert(\n", + " text_encoder,\n", + " \"models/text_encoder.xml\",\n", + " example_input=torch.ones(1, 77, dtype=torch.int64),\n", + " input=((1, 77), ov.Type.i64),\n", + ")\n", + "del text_encoder\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Build a pipeline\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def tensor2vid(video: torch.Tensor, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) -> List[np.ndarray]:\n", + " # This code is copied from https://github.com/modelscope/modelscope/blob/1509fdb973e5871f37148a4b5e5964cafd43e64d/modelscope/pipelines/multi_modal/text_to_video_synthesis_pipeline.py#L78\n", + " # reshape to ncfhw\n", + " mean = torch.tensor(mean, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " std = torch.tensor(std, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " # unnormalize back to [0,1]\n", + " video = video.mul_(std).add_(mean)\n", + " video.clamp_(0, 1)\n", + " # prepare the final outputs\n", + " i, c, f, h, w = video.shape\n", + " images = video.permute(2, 3, 0, 4, 1).reshape(f, h, i * w, c) # 1st (frames, h, batch_size, w, c) 2nd (frames, h, batch_size * w, c)\n", + " images = images.unbind(dim=0) # prepare a list of indvidual (consecutive frames)\n", + " images = [(image.cpu().numpy() * 255).astype(\"uint8\") for image in images] # f h w c\n", + " return images" + ] + }, + { + "cell_type": "code", + "source": [ + "try:\n", + " from diffusers.utils import randn_tensor\n", + "except ImportError:\n", + " from diffusers.utils.torch_utils import randn_tensor\n", + "\n", + "\n", + "class OVTextToVideoSDPipeline(diffusers.DiffusionPipeline):\n", + " def __init__(\n", + " self,\n", + " vae_decoder: ov.CompiledModel,\n", + " text_encoder: ov.CompiledModel,\n", + " tokenizer: transformers.CLIPTokenizer,\n", + " unet: ov.CompiledModel,\n", + " scheduler: diffusers.schedulers.DDIMScheduler,\n", + " ):\n", + " super().__init__()\n", + "\n", + " self.vae_decoder = vae_decoder\n", + " self.text_encoder = text_encoder\n", + " self.tokenizer = tokenizer\n", + " self.unet = unet\n", + " self.scheduler = scheduler\n", + " self.vae_scale_factor = vae_scale_factor\n", + " self.unet_in_channels = unet_in_channels\n", + " self.width = WIDTH\n", + " self.height = HEIGHT\n", + " self.num_frames = NUM_FRAMES\n", + "\n", + " def __call__(\n", + " self,\n", + " prompt: Union[str, List[str]] = None,\n", + " num_inference_steps: int = 50,\n", + " guidance_scale: float = 9.0,\n", + " negative_prompt: Optional[Union[str, List[str]]] = None,\n", + " eta: float = 0.0,\n", + " generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,\n", + " latents: Optional[torch.FloatTensor] = None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " output_type: Optional[str] = \"np\",\n", + " return_dict: bool = True,\n", + " callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None,\n", + " callback_steps: int = 1,\n", + " ):\n", + " r\"\"\"\n", + " Function invoked when calling the pipeline for generation.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts to guide the video generation. If not defined, one has to pass `prompt_embeds`.\n", + " instead.\n", + " num_inference_steps (`int`, *optional*, defaults to 50):\n", + " The number of denoising steps. More denoising steps usually lead to a higher quality videos at the\n", + " expense of slower inference.\n", + " guidance_scale (`float`, *optional*, defaults to 7.5):\n", + " Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).\n", + " `guidance_scale` is defined as `w` of equation 2. of [Imagen\n", + " Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >\n", + " 1`. Higher guidance scale encourages to generate videos that are closely linked to the text `prompt`,\n", + " usually at the expense of lower video quality.\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the video generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " eta (`float`, *optional*, defaults to 0.0):\n", + " Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to\n", + " [`schedulers.DDIMScheduler`], will be ignored for others.\n", + " generator (`torch.Generator` or `List[torch.Generator]`, *optional*):\n", + " One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)\n", + " to make generation deterministic.\n", + " latents (`torch.FloatTensor`, *optional*):\n", + " Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for video\n", + " generation. Can be used to tweak the same generation with different prompts. If not provided, a latents\n", + " tensor will ge generated by sampling using the supplied random `generator`. Latents should be of shape\n", + " `(batch_size, num_channel, num_frames, height, width)`.\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " output_type (`str`, *optional*, defaults to `\"np\"`):\n", + " The output format of the generate video. Choose between `torch.FloatTensor` or `np.array`.\n", + " return_dict (`bool`, *optional*, defaults to `True`):\n", + " Whether or not to return a [`~pipelines.stable_diffusion.TextToVideoSDPipelineOutput`] instead of a\n", + " plain tuple.\n", + " callback (`Callable`, *optional*):\n", + " A function that will be called every `callback_steps` steps during inference. The function will be\n", + " called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`.\n", + " callback_steps (`int`, *optional*, defaults to 1):\n", + " The frequency at which the `callback` function will be called. If not specified, the callback will be\n", + " called at every step.\n", + "\n", + " Returns:\n", + " `List[np.ndarray]`: generated video frames\n", + " \"\"\"\n", + "\n", + " num_images_per_prompt = 1\n", + "\n", + " # 1. Check inputs. Raise error if not correct\n", + " self.check_inputs(\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt,\n", + " prompt_embeds,\n", + " negative_prompt_embeds,\n", + " )\n", + "\n", + " # 2. Define call parameters\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)\n", + " # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`\n", + " # corresponds to doing no classifier free guidance.\n", + " do_classifier_free_guidance = guidance_scale > 1.0\n", + "\n", + " # 3. Encode input prompt\n", + " prompt_embeds = self._encode_prompt(\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt,\n", + " prompt_embeds=prompt_embeds,\n", + " negative_prompt_embeds=negative_prompt_embeds,\n", + " )\n", + "\n", + " # 4. Prepare timesteps\n", + " self.scheduler.set_timesteps(num_inference_steps)\n", + " timesteps = self.scheduler.timesteps\n", + "\n", + " # 5. Prepare latent variables\n", + " num_channels_latents = self.unet_in_channels\n", + " latents = self.prepare_latents(\n", + " batch_size * num_images_per_prompt,\n", + " num_channels_latents,\n", + " prompt_embeds.dtype,\n", + " generator,\n", + " latents,\n", + " )\n", + "\n", + " # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline\n", + " extra_step_kwargs = {\"generator\": generator, \"eta\": eta}\n", + "\n", + " # 7. Denoising loop\n", + " num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order\n", + " with self.progress_bar(total=num_inference_steps) as progress_bar:\n", + " for i, t in enumerate(timesteps):\n", + " # expand the latents if we are doing classifier free guidance\n", + " latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents\n", + " latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)\n", + "\n", + " # predict the noise residual\n", + " noise_pred = self.unet(\n", + " {\n", + " \"sample\": latent_model_input,\n", + " \"timestep\": t,\n", + " \"encoder_hidden_states\": prompt_embeds,\n", + " }\n", + " )[0]\n", + " noise_pred = torch.tensor(noise_pred)\n", + "\n", + " # perform guidance\n", + " if do_classifier_free_guidance:\n", + " noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)\n", + " noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)\n", + "\n", + " # reshape latents\n", + " bsz, channel, frames, width, height = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + " noise_pred = noise_pred.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + "\n", + " # compute the previous noisy sample x_t -> x_t-1\n", + " latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample\n", + "\n", + " # reshape latents back\n", + " latents = latents[None, :].reshape(bsz, frames, channel, width, height).permute(0, 2, 1, 3, 4)\n", + "\n", + " # call the callback, if provided\n", + " if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):\n", + " progress_bar.update()\n", + " if callback is not None and i % callback_steps == 0:\n", + " callback(i, t, latents)\n", + "\n", + " video_tensor = self.decode_latents(latents)\n", + "\n", + " if output_type == \"pt\":\n", + " video = video_tensor\n", + " else:\n", + " video = tensor2vid(video_tensor)\n", + "\n", + " if not return_dict:\n", + " return (video,)\n", + "\n", + " return {\"frames\": video}\n", + "\n", + " # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt\n", + " def _encode_prompt(\n", + " self,\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt=None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " ):\n", + " r\"\"\"\n", + " Encodes the prompt into text encoder hidden states.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " prompt to be encoded\n", + " num_images_per_prompt (`int`):\n", + " number of images that should be generated per prompt\n", + " do_classifier_free_guidance (`bool`):\n", + " whether to use classifier free guidance or not\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the image generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " \"\"\"\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " if prompt_embeds is None:\n", + " text_inputs = self.tokenizer(\n", + " prompt,\n", + " padding=\"max_length\",\n", + " max_length=self.tokenizer.model_max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + " text_input_ids = text_inputs.input_ids\n", + " untruncated_ids = self.tokenizer(prompt, padding=\"longest\", return_tensors=\"pt\").input_ids\n", + "\n", + " if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):\n", + " removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1])\n", + " print(\n", + " \"The following part of your input was truncated because CLIP can only handle sequences up to\"\n", + " f\" {self.tokenizer.model_max_length} tokens: {removed_text}\"\n", + " )\n", + "\n", + " prompt_embeds = self.text_encoder(text_input_ids)\n", + " prompt_embeds = prompt_embeds[0]\n", + " prompt_embeds = torch.tensor(prompt_embeds)\n", + "\n", + " bs_embed, seq_len, _ = prompt_embeds.shape\n", + " # duplicate text embeddings for each generation per prompt, using mps friendly method\n", + " prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # get unconditional embeddings for classifier free guidance\n", + " if do_classifier_free_guidance and negative_prompt_embeds is None:\n", + " uncond_tokens: List[str]\n", + " if negative_prompt is None:\n", + " uncond_tokens = [\"\"] * batch_size\n", + " elif type(prompt) is not type(negative_prompt):\n", + " raise TypeError(f\"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=\" f\" {type(prompt)}.\")\n", + " elif isinstance(negative_prompt, str):\n", + " uncond_tokens = [negative_prompt]\n", + " elif batch_size != len(negative_prompt):\n", + " raise ValueError(\n", + " f\"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:\"\n", + " f\" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches\"\n", + " \" the batch size of `prompt`.\"\n", + " )\n", + " else:\n", + " uncond_tokens = negative_prompt\n", + "\n", + " max_length = prompt_embeds.shape[1]\n", + " uncond_input = self.tokenizer(\n", + " uncond_tokens,\n", + " padding=\"max_length\",\n", + " max_length=max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + "\n", + " negative_prompt_embeds = self.text_encoder(uncond_input.input_ids)\n", + " negative_prompt_embeds = negative_prompt_embeds[0]\n", + " negative_prompt_embeds = torch.tensor(negative_prompt_embeds)\n", + "\n", + " if do_classifier_free_guidance:\n", + " # duplicate unconditional embeddings for each generation per prompt, using mps friendly method\n", + " seq_len = negative_prompt_embeds.shape[1]\n", + "\n", + " negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # For classifier free guidance, we need to do two forward passes.\n", + " # Here we concatenate the unconditional and text embeddings into a single batch\n", + " # to avoid doing two forward passes\n", + " prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])\n", + "\n", + " return prompt_embeds\n", + "\n", + " def prepare_latents(\n", + " self,\n", + " batch_size,\n", + " num_channels_latents,\n", + " dtype,\n", + " generator,\n", + " latents=None,\n", + " ):\n", + " shape = (\n", + " batch_size,\n", + " num_channels_latents,\n", + " self.num_frames,\n", + " self.height // self.vae_scale_factor,\n", + " self.width // self.vae_scale_factor,\n", + " )\n", + " if isinstance(generator, list) and len(generator) != batch_size:\n", + " raise ValueError(\n", + " f\"You have passed a list of generators of length {len(generator)}, but requested an effective batch\"\n", + " f\" size of {batch_size}. Make sure the batch size matches the length of the generators.\"\n", + " )\n", + "\n", + " if latents is None:\n", + " latents = randn_tensor(shape, generator=generator, dtype=dtype)\n", + "\n", + " # scale the initial noise by the standard deviation required by the scheduler\n", + " latents = latents * self.scheduler.init_noise_sigma\n", + " return latents\n", + "\n", + " def check_inputs(\n", + " self,\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt=None,\n", + " prompt_embeds=None,\n", + " negative_prompt_embeds=None,\n", + " ):\n", + " if self.height % 8 != 0 or self.width % 8 != 0:\n", + " raise ValueError(f\"`height` and `width` have to be divisible by 8 but are {self.height} and {self.width}.\")\n", + "\n", + " if (callback_steps is None) or (callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0)):\n", + " raise ValueError(f\"`callback_steps` has to be a positive integer but is {callback_steps} of type\" f\" {type(callback_steps)}.\")\n", + "\n", + " if prompt is not None and prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to\" \" only forward one of the two.\"\n", + " )\n", + " elif prompt is None and prompt_embeds is None:\n", + " raise ValueError(\"Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined.\")\n", + " elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):\n", + " raise ValueError(f\"`prompt` has to be of type `str` or `list` but is {type(prompt)}\")\n", + "\n", + " if negative_prompt is not None and negative_prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:\"\n", + " f\" {negative_prompt_embeds}. Please make sure to only forward one of the two.\"\n", + " )\n", + "\n", + " if prompt_embeds is not None and negative_prompt_embeds is not None:\n", + " if prompt_embeds.shape != negative_prompt_embeds.shape:\n", + " raise ValueError(\n", + " \"`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but\"\n", + " f\" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`\"\n", + " f\" {negative_prompt_embeds.shape}.\"\n", + " )\n", + "\n", + " def decode_latents(self, latents):\n", + " scale_factor = 0.18215\n", + " latents = 1 / scale_factor * latents\n", + "\n", + " batch_size, channels, num_frames, height, width = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(batch_size * num_frames, channels, height, width)\n", + " image = self.vae_decoder(latents)[0]\n", + " image = torch.tensor(image)\n", + " video = (\n", + " image[None, :]\n", + " .reshape(\n", + " (\n", + " batch_size,\n", + " num_frames,\n", + " -1,\n", + " )\n", + " + image.shape[2:]\n", + " )\n", + " .permute(0, 2, 1, 3, 4)\n", + " )\n", + " # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16\n", + " video = video.float()\n", + " return video" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Inference with OpenVINO\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "from notebook_utils import device_widget\n", + "\n", + "device = device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_unet = core.compile_model(unet_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_vae_decoder = core.compile_model(vae_decoder_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_text_encoder = core.compile_model(text_encoder_xml, device_name=device.value)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Here we replace the pipeline parts with versions converted to OpenVINO IR and compiled to specific device. Note that we use original pipeline tokenizer and scheduler." + ] + }, + { + "cell_type": "code", + "source": [ + "ov_pipe = OVTextToVideoSDPipeline(ov_vae_decoder, ov_text_encoder, tokenizer, ov_unet, scheduler)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Define a prompt\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "prompt = \"A panda eating bamboo on a rock.\"" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Let's generate a video for our prompt. For full list of arguments, see `__call__` function definition of `OVTextToVideoSDPipeline` class in [Build a pipeline](#Build-a-pipeline) section." + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Video generation\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "frames = ov_pipe(prompt, num_inference_steps=25)[\"frames\"]" + ] + }, + { + "cell_type": "code", + "source": [ + "images = [PIL.Image.fromarray(frame) for frame in frames]\n", + "images[0].save(\"output.gif\", save_all=True, append_images=images[1:], duration=125, loop=0)\n", + "with open(\"output.gif\", \"rb\") as gif_file:\n", + " b64 = f\"data:image/gif;base64,{base64.b64encode(gif_file.read()).decode()}\"\n", + "IPython.display.HTML(f'')" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Interactive demo\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def generate(prompt, seed, num_inference_steps, _=gr.Progress(track_tqdm=True)):\n", + " generator = torch.Generator().manual_seed(seed)\n", + " frames = ov_pipe(\n", + " prompt,\n", + " num_inference_steps=num_inference_steps,\n", + " generator=generator,\n", + " )[\"frames\"]\n", + " out_file = tempfile.NamedTemporaryFile(suffix=\".gif\", delete=False)\n", + " images = [PIL.Image.fromarray(frame) for frame in frames]\n", + " images[0].save(out_file, save_all=True, append_images=images[1:], duration=125, loop=0)\n", + " return out_file.name\n", + "\n", + "\n", + "demo = gr.Interface(\n", + " generate,\n", + " [\n", + " gr.Textbox(label=\"Prompt\"),\n", + " gr.Slider(0, 1000000, value=42, label=\"Seed\", step=1),\n", + " gr.Slider(10, 50, value=25, label=\"Number of inference steps\", step=1),\n", + " ],\n", + " gr.Image(label=\"Result\"),\n", + " examples=[\n", + " [\"An astronaut riding a horse.\", 0, 25],\n", + " [\"A panda eating bamboo on a rock.\", 0, 25],\n", + " [\"Spiderman is surfing.\", 0, 25],\n", + " ],\n", + " allow_flagging=\"never\",\n", + ")\n", + "\n", + "try:\n", + " demo.queue().launch(debug=True)\n", + "except Exception:\n", + " demo.queue().launch(share=True, debug=True)\n", + "# if you are launching remotely, specify server_name and server_port\n", + "# demo.launch(server_name='your server name', server_port='server port in int')\n", + "# Read more in the docs: https://gradio.app/docs/" + ] + } + ].map(fromJupyterCell) + , + [ + { + "cell_type": "markdown", + "source": [ + "# Video generation with ZeroScope and OpenVINO\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Install and import required packages](#Install-and-import-required-packages)\n", + "- [Load the model](#Load-the-model)\n", + "- [Convert the model](#Convert-the-model)\n", + " - [Define the conversion function](#Define-the-conversion-function)\n", + " - [UNet](#UNet)\n", + " - [VAE](#VAE)\n", + " - [Text encoder](#Text-encoder)\n", + "- [Build a pipeline](#Build-a-pipeline)\n", + "- [Inference with OpenVINO](#Inference-with-OpenVINO)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Define a prompt](#Define-a-prompt)\n", + " - [Video generation](#Video-generation)\n", + "- [Interactive demo](#Interactive-demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The ZeroScope model is a free and open-source text-to-video model that can generate realistic and engaging videos from text descriptions. It is based on the [Modelscope](https://modelscope.cn/models/damo/text-to-video-synthesis/summary) model, but it has been improved to produce higher-quality videos with a 16:9 aspect ratio and no Shutterstock watermark. The ZeroScope model is available in two versions: ZeroScope_v2 576w, which is optimized for rapid content creation at a resolution of 576x320 pixels, and ZeroScope_v2 XL, which upscales videos to a high-definition resolution of 1024x576.\n", + "\n", + "The ZeroScope model is trained on a dataset of over 9,000 videos and 29,000 tagged frames. It uses a diffusion model to generate videos, which means that it starts with a random noise image and gradually adds detail to it until it matches the text description. The ZeroScope model is still under development, but it has already been used to create some impressive videos. For example, it has been used to create videos of people dancing, playing sports, and even driving cars.\n", + "\n", + "The ZeroScope model is a powerful tool that can be used to create various videos, from simple animations to complex scenes. It is still under development, but it has the potential to revolutionize the way we create and consume video content.\n", + "\n", + "Both versions of the ZeroScope model are available on Hugging Face:\n", + " - [ZeroScope_v2 576w](https://huggingface.co/cerspense/zeroscope_v2_576w)\n", + " - [ZeroScope_v2 XL](https://huggingface.co/cerspense/zeroscope_v2_XL)\n", + "\n", + "We will use the first one." + ] + }, + { + "cell_type": "markdown", + "source": [ + "
\n", + " This tutorial requires at least 24GB of free memory to generate a video with a frame size of 432x240 and 16 frames. Increasing either of these values will require more memory and take more time.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Install and import required packages\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "To work with text-to-video synthesis model, we will use Hugging Face's [Diffusers](https://github.com/huggingface/diffusers) library. It provides already pretrained model from `cerspense`." + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install -q --extra-index-url https://download.pytorch.org/whl/cpu \"diffusers>=0.18.0\" \"torch>=2.1\" transformers \"openvino>=2023.1.0\" numpy \"gradio>=4.19\"" + ] + }, + { + "cell_type": "code", + "source": [ + "import gc\n", + "from typing import Optional, Union, List, Callable\n", + "import base64\n", + "import tempfile\n", + "import warnings\n", + "\n", + "import diffusers\n", + "import transformers\n", + "import numpy as np\n", + "import IPython\n", + "import torch\n", + "import PIL\n", + "import gradio as gr\n", + "\n", + "import openvino as ov" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Original 576x320 inference requires a lot of RAM (>100GB), so let's run our example on a smaller frame size, keeping the same aspect ratio. Try reducing values below to reduce the memory consumption." + ] + }, + { + "cell_type": "code", + "source": [ + "WIDTH = 432 # must be divisible by 8\n", + "HEIGHT = 240 # must be divisible by 8\n", + "NUM_FRAMES = 16" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The model is loaded from HuggingFace using `.from_pretrained` method of `diffusers.DiffusionPipeline`." + ] + }, + { + "cell_type": "code", + "source": [ + "pipe = diffusers.DiffusionPipeline.from_pretrained(\"cerspense/zeroscope_v2_576w\")" + ] + }, + { + "cell_type": "code", + "source": [ + "unet = pipe.unet\n", + "unet.eval()\n", + "vae = pipe.vae\n", + "vae.eval()\n", + "text_encoder = pipe.text_encoder\n", + "text_encoder.eval()\n", + "tokenizer = pipe.tokenizer\n", + "scheduler = pipe.scheduler\n", + "vae_scale_factor = pipe.vae_scale_factor\n", + "unet_in_channels = pipe.unet.config.in_channels\n", + "sample_width = WIDTH // vae_scale_factor\n", + "sample_height = HEIGHT // vae_scale_factor\n", + "del pipe\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Convert the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The architecture for generating videos from text comprises three distinct sub-networks: one for extracting text features, another for translating text features into the video latent space using a diffusion model, and a final one for mapping the video latent space to the visual space. The collective parameters of the entire model amount to approximately 1.7 billion. It's capable of processing English input. The diffusion model is built upon the Unet3D model and achieves video generation by iteratively denoising a starting point of pure Gaussian noise video." + ] + }, + { + "cell_type": "markdown", + "source": [] + }, + { + "cell_type": "markdown", + "source": [ + "### Define the conversion function\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Model components are PyTorch modules, that can be converted with `ov.convert_model` function directly. We also use `ov.save_model` function to serialize the result of conversion." + ] + }, + { + "cell_type": "code", + "source": [ + "warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)" + ] + }, + { + "cell_type": "code", + "source": [ + "from pathlib import Path\n", + "\n", + "\n", + "def convert(model: torch.nn.Module, xml_path: str, **convert_kwargs) -> Path:\n", + " xml_path = Path(xml_path)\n", + " if not xml_path.exists():\n", + " xml_path.parent.mkdir(parents=True, exist_ok=True)\n", + " with torch.no_grad():\n", + " converted_model = ov.convert_model(model, **convert_kwargs)\n", + " ov.save_model(converted_model, xml_path)\n", + " del converted_model\n", + " gc.collect()\n", + " torch._C._jit_clear_class_registry()\n", + " torch.jit._recursive.concrete_type_store = torch.jit._recursive.ConcreteTypeStore()\n", + " torch.jit._state._clear_class_state()\n", + " return xml_path" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### UNet\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text-to-video generation pipeline main component is a conditional 3D UNet model that takes a noisy sample, conditional state, and a timestep and returns a sample shaped output." + ] + }, + { + "cell_type": "code", + "source": [ + "unet_xml_path = convert(\n", + " unet,\n", + " \"models/unet.xml\",\n", + " example_input={\n", + " \"sample\": torch.randn(2, 4, 2, int(sample_height // 2), int(sample_width // 2)),\n", + " \"timestep\": torch.tensor(1),\n", + " \"encoder_hidden_states\": torch.randn(2, 77, 1024),\n", + " },\n", + " input=[\n", + " (\"sample\", (2, 4, NUM_FRAMES, sample_height, sample_width)),\n", + " (\"timestep\", ()),\n", + " (\"encoder_hidden_states\", (2, 77, 1024)),\n", + " ],\n", + ")\n", + "del unet\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### VAE\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Variational autoencoder (VAE) uses UNet output to decode latents to visual representations. Our VAE model has KL loss for encoding images into latents and decoding latent representations into images. For inference, we need only decoder part." + ] + }, + { + "cell_type": "code", + "source": [ + "class VaeDecoderWrapper(torch.nn.Module):\n", + " def __init__(self, vae):\n", + " super().__init__()\n", + " self.vae = vae\n", + "\n", + " def forward(self, z: torch.FloatTensor):\n", + " return self.vae.decode(z)" + ] + }, + { + "cell_type": "code", + "source": [ + "vae_decoder_xml_path = convert(\n", + " VaeDecoderWrapper(vae),\n", + " \"models/vae.xml\",\n", + " example_input=torch.randn(2, 4, 32, 32),\n", + " input=((NUM_FRAMES, 4, sample_height, sample_width)),\n", + ")\n", + "del vae\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Text encoder\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text encoder is used to encode the input prompt to tensor. Default tensor length is 77." + ] + }, + { + "cell_type": "code", + "source": [ + "text_encoder_xml = convert(\n", + " text_encoder,\n", + " \"models/text_encoder.xml\",\n", + " example_input=torch.ones(1, 77, dtype=torch.int64),\n", + " input=((1, 77), ov.Type.i64),\n", + ")\n", + "del text_encoder\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Build a pipeline\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def tensor2vid(video: torch.Tensor, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) -> List[np.ndarray]:\n", + " # This code is copied from https://github.com/modelscope/modelscope/blob/1509fdb973e5871f37148a4b5e5964cafd43e64d/modelscope/pipelines/multi_modal/text_to_video_synthesis_pipeline.py#L78\n", + " # reshape to ncfhw\n", + " mean = torch.tensor(mean, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " std = torch.tensor(std, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " # unnormalize back to [0,1]\n", + " video = video.mul_(std).add_(mean)\n", + " video.clamp_(0, 1)\n", + " # prepare the final outputs\n", + " i, c, f, h, w = video.shape\n", + " images = video.permute(2, 3, 0, 4, 1).reshape(f, h, i * w, c) # 1st (frames, h, batch_size, w, c) 2nd (frames, h, batch_size * w, c)\n", + " images = images.unbind(dim=0) # prepare a list of indvidual (consecutive frames)\n", + " images = [(image.cpu().numpy() * 255).astype(\"uint8\") for image in images] # f h w c\n", + " return images" + ] + }, + { + "cell_type": "code", + "source": [ + "try:\n", + " from diffusers.utils import randn_tensor\n", + "except ImportError:\n", + " from diffusers.utils.torch_utils import randn_tensor\n", + "\n", + "\n", + "class OVTextToVideoSDPipeline(diffusers.DiffusionPipeline):\n", + " def __init__(\n", + " self,\n", + " vae_decoder: ov.CompiledModel,\n", + " text_encoder: ov.CompiledModel,\n", + " tokenizer: transformers.CLIPTokenizer,\n", + " unet: ov.CompiledModel,\n", + " scheduler: diffusers.schedulers.DDIMScheduler,\n", + " ):\n", + " super().__init__()\n", + "\n", + " self.vae_decoder = vae_decoder\n", + " self.text_encoder = text_encoder\n", + " self.tokenizer = tokenizer\n", + " self.unet = unet\n", + " self.scheduler = scheduler\n", + " self.vae_scale_factor = vae_scale_factor\n", + " self.unet_in_channels = unet_in_channels\n", + " self.width = WIDTH\n", + " self.height = HEIGHT\n", + " self.num_frames = NUM_FRAMES\n", + "\n", + " def __call__(\n", + " self,\n", + " prompt: Union[str, List[str]] = None,\n", + " num_inference_steps: int = 50,\n", + " guidance_scale: float = 9.0,\n", + " negative_prompt: Optional[Union[str, List[str]]] = None,\n", + " eta: float = 0.0,\n", + " generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,\n", + " latents: Optional[torch.FloatTensor] = None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " output_type: Optional[str] = \"np\",\n", + " return_dict: bool = True,\n", + " callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None,\n", + " callback_steps: int = 1,\n", + " ):\n", + " r\"\"\"\n", + " Function invoked when calling the pipeline for generation.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts to guide the video generation. If not defined, one has to pass `prompt_embeds`.\n", + " instead.\n", + " num_inference_steps (`int`, *optional*, defaults to 50):\n", + " The number of denoising steps. More denoising steps usually lead to a higher quality videos at the\n", + " expense of slower inference.\n", + " guidance_scale (`float`, *optional*, defaults to 7.5):\n", + " Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).\n", + " `guidance_scale` is defined as `w` of equation 2. of [Imagen\n", + " Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >\n", + " 1`. Higher guidance scale encourages to generate videos that are closely linked to the text `prompt`,\n", + " usually at the expense of lower video quality.\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the video generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " eta (`float`, *optional*, defaults to 0.0):\n", + " Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to\n", + " [`schedulers.DDIMScheduler`], will be ignored for others.\n", + " generator (`torch.Generator` or `List[torch.Generator]`, *optional*):\n", + " One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)\n", + " to make generation deterministic.\n", + " latents (`torch.FloatTensor`, *optional*):\n", + " Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for video\n", + " generation. Can be used to tweak the same generation with different prompts. If not provided, a latents\n", + " tensor will ge generated by sampling using the supplied random `generator`. Latents should be of shape\n", + " `(batch_size, num_channel, num_frames, height, width)`.\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " output_type (`str`, *optional*, defaults to `\"np\"`):\n", + " The output format of the generate video. Choose between `torch.FloatTensor` or `np.array`.\n", + " return_dict (`bool`, *optional*, defaults to `True`):\n", + " Whether or not to return a [`~pipelines.stable_diffusion.TextToVideoSDPipelineOutput`] instead of a\n", + " plain tuple.\n", + " callback (`Callable`, *optional*):\n", + " A function that will be called every `callback_steps` steps during inference. The function will be\n", + " called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`.\n", + " callback_steps (`int`, *optional*, defaults to 1):\n", + " The frequency at which the `callback` function will be called. If not specified, the callback will be\n", + " called at every step.\n", + "\n", + " Returns:\n", + " `List[np.ndarray]`: generated video frames\n", + " \"\"\"\n", + "\n", + " num_images_per_prompt = 1\n", + "\n", + " # 1. Check inputs. Raise error if not correct\n", + " self.check_inputs(\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt,\n", + " prompt_embeds,\n", + " negative_prompt_embeds,\n", + " )\n", + "\n", + " # 2. Define call parameters\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)\n", + " # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`\n", + " # corresponds to doing no classifier free guidance.\n", + " do_classifier_free_guidance = guidance_scale > 1.0\n", + "\n", + " # 3. Encode input prompt\n", + " prompt_embeds = self._encode_prompt(\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt,\n", + " prompt_embeds=prompt_embeds,\n", + " negative_prompt_embeds=negative_prompt_embeds,\n", + " )\n", + "\n", + " # 4. Prepare timesteps\n", + " self.scheduler.set_timesteps(num_inference_steps)\n", + " timesteps = self.scheduler.timesteps\n", + "\n", + " # 5. Prepare latent variables\n", + " num_channels_latents = self.unet_in_channels\n", + " latents = self.prepare_latents(\n", + " batch_size * num_images_per_prompt,\n", + " num_channels_latents,\n", + " prompt_embeds.dtype,\n", + " generator,\n", + " latents,\n", + " )\n", + "\n", + " # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline\n", + " extra_step_kwargs = {\"generator\": generator, \"eta\": eta}\n", + "\n", + " # 7. Denoising loop\n", + " num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order\n", + " with self.progress_bar(total=num_inference_steps) as progress_bar:\n", + " for i, t in enumerate(timesteps):\n", + " # expand the latents if we are doing classifier free guidance\n", + " latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents\n", + " latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)\n", + "\n", + " # predict the noise residual\n", + " noise_pred = self.unet(\n", + " {\n", + " \"sample\": latent_model_input,\n", + " \"timestep\": t,\n", + " \"encoder_hidden_states\": prompt_embeds,\n", + " }\n", + " )[0]\n", + " noise_pred = torch.tensor(noise_pred)\n", + "\n", + " # perform guidance\n", + " if do_classifier_free_guidance:\n", + " noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)\n", + " noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)\n", + "\n", + " # reshape latents\n", + " bsz, channel, frames, width, height = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + " noise_pred = noise_pred.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + "\n", + " # compute the previous noisy sample x_t -> x_t-1\n", + " latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample\n", + "\n", + " # reshape latents back\n", + " latents = latents[None, :].reshape(bsz, frames, channel, width, height).permute(0, 2, 1, 3, 4)\n", + "\n", + " # call the callback, if provided\n", + " if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):\n", + " progress_bar.update()\n", + " if callback is not None and i % callback_steps == 0:\n", + " callback(i, t, latents)\n", + "\n", + " video_tensor = self.decode_latents(latents)\n", + "\n", + " if output_type == \"pt\":\n", + " video = video_tensor\n", + " else:\n", + " video = tensor2vid(video_tensor)\n", + "\n", + " if not return_dict:\n", + " return (video,)\n", + "\n", + " return {\"frames\": video}\n", + "\n", + " # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt\n", + " def _encode_prompt(\n", + " self,\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt=None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " ):\n", + " r\"\"\"\n", + " Encodes the prompt into text encoder hidden states.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " prompt to be encoded\n", + " num_images_per_prompt (`int`):\n", + " number of images that should be generated per prompt\n", + " do_classifier_free_guidance (`bool`):\n", + " whether to use classifier free guidance or not\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the image generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " \"\"\"\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " if prompt_embeds is None:\n", + " text_inputs = self.tokenizer(\n", + " prompt,\n", + " padding=\"max_length\",\n", + " max_length=self.tokenizer.model_max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + " text_input_ids = text_inputs.input_ids\n", + " untruncated_ids = self.tokenizer(prompt, padding=\"longest\", return_tensors=\"pt\").input_ids\n", + "\n", + " if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):\n", + " removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1])\n", + " print(\n", + " \"The following part of your input was truncated because CLIP can only handle sequences up to\"\n", + " f\" {self.tokenizer.model_max_length} tokens: {removed_text}\"\n", + " )\n", + "\n", + " prompt_embeds = self.text_encoder(text_input_ids)\n", + " prompt_embeds = prompt_embeds[0]\n", + " prompt_embeds = torch.tensor(prompt_embeds)\n", + "\n", + " bs_embed, seq_len, _ = prompt_embeds.shape\n", + " # duplicate text embeddings for each generation per prompt, using mps friendly method\n", + " prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # get unconditional embeddings for classifier free guidance\n", + " if do_classifier_free_guidance and negative_prompt_embeds is None:\n", + " uncond_tokens: List[str]\n", + " if negative_prompt is None:\n", + " uncond_tokens = [\"\"] * batch_size\n", + " elif type(prompt) is not type(negative_prompt):\n", + " raise TypeError(f\"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=\" f\" {type(prompt)}.\")\n", + " elif isinstance(negative_prompt, str):\n", + " uncond_tokens = [negative_prompt]\n", + " elif batch_size != len(negative_prompt):\n", + " raise ValueError(\n", + " f\"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:\"\n", + " f\" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches\"\n", + " \" the batch size of `prompt`.\"\n", + " )\n", + " else:\n", + " uncond_tokens = negative_prompt\n", + "\n", + " max_length = prompt_embeds.shape[1]\n", + " uncond_input = self.tokenizer(\n", + " uncond_tokens,\n", + " padding=\"max_length\",\n", + " max_length=max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + "\n", + " negative_prompt_embeds = self.text_encoder(uncond_input.input_ids)\n", + " negative_prompt_embeds = negative_prompt_embeds[0]\n", + " negative_prompt_embeds = torch.tensor(negative_prompt_embeds)\n", + "\n", + " if do_classifier_free_guidance:\n", + " # duplicate unconditional embeddings for each generation per prompt, using mps friendly method\n", + " seq_len = negative_prompt_embeds.shape[1]\n", + "\n", + " negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # For classifier free guidance, we need to do two forward passes.\n", + " # Here we concatenate the unconditional and text embeddings into a single batch\n", + " # to avoid doing two forward passes\n", + " prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])\n", + "\n", + " return prompt_embeds\n", + "\n", + " def prepare_latents(\n", + " self,\n", + " batch_size,\n", + " num_channels_latents,\n", + " dtype,\n", + " generator,\n", + " latents=None,\n", + " ):\n", + " shape = (\n", + " batch_size,\n", + " num_channels_latents,\n", + " self.num_frames,\n", + " self.height // self.vae_scale_factor,\n", + " self.width // self.vae_scale_factor,\n", + " )\n", + " if isinstance(generator, list) and len(generator) != batch_size:\n", + " raise ValueError(\n", + " f\"You have passed a list of generators of length {len(generator)}, but requested an effective batch\"\n", + " f\" size of {batch_size}. Make sure the batch size matches the length of the generators.\"\n", + " )\n", + "\n", + " if latents is None:\n", + " latents = randn_tensor(shape, generator=generator, dtype=dtype)\n", + "\n", + " # scale the initial noise by the standard deviation required by the scheduler\n", + " latents = latents * self.scheduler.init_noise_sigma\n", + " return latents\n", + "\n", + " def check_inputs(\n", + " self,\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt=None,\n", + " prompt_embeds=None,\n", + " negative_prompt_embeds=None,\n", + " ):\n", + " if self.height % 8 != 0 or self.width % 8 != 0:\n", + " raise ValueError(f\"`height` and `width` have to be divisible by 8 but are {self.height} and {self.width}.\")\n", + "\n", + " if (callback_steps is None) or (callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0)):\n", + " raise ValueError(f\"`callback_steps` has to be a positive integer but is {callback_steps} of type\" f\" {type(callback_steps)}.\")\n", + "\n", + " if prompt is not None and prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to\" \" only forward one of the two.\"\n", + " )\n", + " elif prompt is None and prompt_embeds is None:\n", + " raise ValueError(\"Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined.\")\n", + " elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):\n", + " raise ValueError(f\"`prompt` has to be of type `str` or `list` but is {type(prompt)}\")\n", + "\n", + " if negative_prompt is not None and negative_prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:\"\n", + " f\" {negative_prompt_embeds}. Please make sure to only forward one of the two.\"\n", + " )\n", + "\n", + " if prompt_embeds is not None and negative_prompt_embeds is not None:\n", + " if prompt_embeds.shape != negative_prompt_embeds.shape:\n", + " raise ValueError(\n", + " \"`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but\"\n", + " f\" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`\"\n", + " f\" {negative_prompt_embeds.shape}.\"\n", + " )\n", + "\n", + " def decode_latents(self, latents):\n", + " scale_factor = 0.18215\n", + " latents = 1 / scale_factor * latents\n", + "\n", + " batch_size, channels, num_frames, height, width = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(batch_size * num_frames, channels, height, width)\n", + " image = self.vae_decoder(latents)[0]\n", + " image = torch.tensor(image)\n", + " video = (\n", + " image[None, :]\n", + " .reshape(\n", + " (\n", + " batch_size,\n", + " num_frames,\n", + " -1,\n", + " )\n", + " + image.shape[2:]\n", + " )\n", + " .permute(0, 2, 1, 3, 4)\n", + " )\n", + " # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16\n", + " video = video.float()\n", + " return video" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Inference with OpenVINO\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "from notebook_utils import device_widget\n", + "\n", + "device = device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_unet = core.compile_model(unet_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_vae_decoder = core.compile_model(vae_decoder_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_text_encoder = core.compile_model(text_encoder_xml, device_name=device.value)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Here we replace the pipeline parts with versions converted to OpenVINO IR and compiled to specific device. Note that we use original pipeline tokenizer and scheduler." + ] + }, + { + "cell_type": "code", + "source": [ + "ov_pipe = OVTextToVideoSDPipeline(ov_vae_decoder, ov_text_encoder, tokenizer, ov_unet, scheduler)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Define a prompt\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "prompt = \"A panda eating bamboo on a rock.\"" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Let's generate a video for our prompt. For full list of arguments, see `__call__` function definition of `OVTextToVideoSDPipeline` class in [Build a pipeline](#Build-a-pipeline) section." + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Video generation\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "frames = ov_pipe(prompt, num_inference_steps=25)[\"frames\"]" + ] + }, + { + "cell_type": "code", + "source": [ + "images = [PIL.Image.fromarray(frame) for frame in frames]\n", + "images[0].save(\"output.gif\", save_all=True, append_images=images[1:], duration=125, loop=0)\n", + "with open(\"output.gif\", \"rb\") as gif_file:\n", + " b64 = f\"data:image/gif;base64,{base64.b64encode(gif_file.read()).decode()}\"\n", + "IPython.display.HTML(f'')" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Interactive demo\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "metadata": { + "test_replace": { + " demo.queue().launch(debug=True)": " demo.queue.launch()", + " demo.queue().launch(share=True, debug=True)": " demo.queue().launch(share=True)" + } + }, + "source": [ + "def generate(prompt, seed, num_inference_steps, _=gr.Progress(track_tqdm=True)):\n", + " generator = torch.Generator().manual_seed(seed)\n", + " frames = ov_pipe(\n", + " prompt,\n", + " num_inference_steps=num_inference_steps,\n", + " generator=generator,\n", + " )[\"frames\"]\n", + " out_file = tempfile.NamedTemporaryFile(suffix=\".gif\", delete=False)\n", + " images = [PIL.Image.fromarray(frame) for frame in frames]\n", + " images[0].save(out_file, save_all=True, append_images=images[1:], duration=125, loop=0)\n", + " return out_file.name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "if not Path(\"gradio_helper.py\").exists():\n", + " r = requests.get(url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/notebooks/zeroscope-text2video/gradio_helper.py\")\n", + " open(\"gradio_helper.py\", \"w\").write(r.text)\n", + "\n", + "from gradio_helper import make_demo\n", + "\n", + "demo = make_demo(fn=generate)\n", + "\n", + "try:\n", + " demo.queue().launch(debug=True)\n", + "except Exception:\n", + " demo.queue().launch(share=True, debug=True)\n", + "# If you are launching remotely, specify server_name and server_port\n", + "# EXAMPLE: `demo.launch(server_name='your server name', server_port='server port in int')`\n", + "# To learn more please refer to the Gradio docs: https://gradio.app/docs/" + ] + }, + { + "cell_type": "code", + "source": [ + "# please uncomment and run this cell for stopping gradio interface\n", + "# demo.close()" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + { modified: 24, original: 24 }, + { modified: 25, original: 25 }, + { modified: 26, original: 26 }, + { modified: 27, original: 27 }, + { modified: 28, original: 28 }, + { modified: 29, original: 29 }, + { modified: 30, original: 30 }, + { modified: 31, original: 31 }, + { modified: 32, original: 32 }, + { modified: 33, original: 33 }, + { modified: 34, original: 34 }, + { modified: 35, original: 35 }, + { modified: 36, original: 36 }, + { modified: 37, original: 37 }, + { modified: 38, original: 38 }, + { modified: 39, original: 39 }, + { modified: 40, original: 40 }, + { modified: 41, original: 41 }, + { modified: 42, original: 42 }, + { modified: 43, original: 43 }, + { modified: 44, original: 44 }, + { modified: 45, original: 45 }, + { modified: 46, original: 46 }, + { modified: 47, original: 47 }, + { modified: 48, original: 48 }, + { modified: 49, original: 49 }, + { modified: 50, original: -1 }, + { modified: 51, original: -1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 49, originalLength: 1, modifiedStart: 49, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 50, originalLength: 0, modifiedStart: 50, modifiedLength: 2 } satisfies IDiffChange, + ]); + }); + test('Modification of three cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "source": [ + "# Video generation with ZeroScope and OpenVINO\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Install and import required packages](#Install-and-import-required-packages)\n", + "- [Load the model](#Load-the-model)\n", + "- [Convert the model](#Convert-the-model)\n", + " - [Define the conversion function](#Define-the-conversion-function)\n", + " - [UNet](#UNet)\n", + " - [VAE](#VAE)\n", + " - [Text encoder](#Text-encoder)\n", + "- [Build a pipeline](#Build-a-pipeline)\n", + "- [Inference with OpenVINO](#Inference-with-OpenVINO)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Define a prompt](#Define-a-prompt)\n", + " - [Video generation](#Video-generation)\n", + "- [Interactive demo](#Interactive-demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The ZeroScope model is a free and open-source text-to-video model that can generate realistic and engaging videos from text descriptions. It is based on the [Modelscope](https://modelscope.cn/models/damo/text-to-video-synthesis/summary) model, but it has been improved to produce higher-quality videos with a 16:9 aspect ratio and no Shutterstock watermark. The ZeroScope model is available in two versions: ZeroScope_v2 576w, which is optimized for rapid content creation at a resolution of 576x320 pixels, and ZeroScope_v2 XL, which upscales videos to a high-definition resolution of 1024x576.\n", + "\n", + "The ZeroScope model is trained on a dataset of over 9,000 videos and 29,000 tagged frames. It uses a diffusion model to generate videos, which means that it starts with a random noise image and gradually adds detail to it until it matches the text description. The ZeroScope model is still under development, but it has already been used to create some impressive videos. For example, it has been used to create videos of people dancing, playing sports, and even driving cars.\n", + "\n", + "The ZeroScope model is a powerful tool that can be used to create various videos, from simple animations to complex scenes. It is still under development, but it has the potential to revolutionize the way we create and consume video content.\n", + "\n", + "Both versions of the ZeroScope model are available on Hugging Face:\n", + " - [ZeroScope_v2 576w](https://huggingface.co/cerspense/zeroscope_v2_576w)\n", + " - [ZeroScope_v2 XL](https://huggingface.co/cerspense/zeroscope_v2_XL)\n", + "\n", + "We will use the first one." + ] + }, + { + "cell_type": "markdown", + "source": [ + "
\n", + " This tutorial requires at least 24GB of free memory to generate a video with a frame size of 432x240 and 16 frames. Increasing either of these values will require more memory and take more time.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Install and import required packages\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "To work with text-to-video synthesis model, we will use Hugging Face's [Diffusers](https://github.com/huggingface/diffusers) library. It provides already pretrained model from `cerspense`." + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install -q --extra-index-url https://download.pytorch.org/whl/cpu \"diffusers>=0.18.0\" \"torch>=2.1\" transformers \"openvino>=2023.1.0\" numpy \"gradio>=4.19\"" + ] + }, + { + "cell_type": "code", + "source": [ + "import gc\n", + "from typing import Optional, Union, List, Callable\n", + "import base64\n", + "import tempfile\n", + "import warnings\n", + "\n", + "import diffusers\n", + "import transformers\n", + "import numpy as np\n", + "import IPython\n", + "import ipywidgets as widgets\n", + "import torch\n", + "import PIL\n", + "import gradio as gr\n", + "\n", + "import openvino as ov" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Original 576x320 inference requires a lot of RAM (>100GB), so let's run our example on a smaller frame size, keeping the same aspect ratio. Try reducing values below to reduce the memory consumption." + ] + }, + { + "cell_type": "code", + "source": [ + "WIDTH = 432 # must be divisible by 8\n", + "HEIGHT = 240 # must be divisible by 8\n", + "NUM_FRAMES = 16" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The model is loaded from HuggingFace using `.from_pretrained` method of `diffusers.DiffusionPipeline`." + ] + }, + { + "cell_type": "code", + "source": [ + "pipe = diffusers.DiffusionPipeline.from_pretrained(\"cerspense/zeroscope_v2_576w\")" + ] + }, + { + "cell_type": "code", + "source": [ + "unet = pipe.unet\n", + "unet.eval()\n", + "vae = pipe.vae\n", + "vae.eval()\n", + "text_encoder = pipe.text_encoder\n", + "text_encoder.eval()\n", + "tokenizer = pipe.tokenizer\n", + "scheduler = pipe.scheduler\n", + "vae_scale_factor = pipe.vae_scale_factor\n", + "unet_in_channels = pipe.unet.config.in_channels\n", + "sample_width = WIDTH // vae_scale_factor\n", + "sample_height = HEIGHT // vae_scale_factor\n", + "del pipe\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Convert the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The architecture for generating videos from text comprises three distinct sub-networks: one for extracting text features, another for translating text features into the video latent space using a diffusion model, and a final one for mapping the video latent space to the visual space. The collective parameters of the entire model amount to approximately 1.7 billion. It's capable of processing English input. The diffusion model is built upon the Unet3D model and achieves video generation by iteratively denoising a starting point of pure Gaussian noise video." + ] + }, + { + "cell_type": "markdown", + "source": [] + }, + { + "cell_type": "markdown", + "source": [ + "### Define the conversion function\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Model components are PyTorch modules, that can be converted with `ov.convert_model` function directly. We also use `ov.save_model` function to serialize the result of conversion." + ] + }, + { + "cell_type": "code", + "source": [ + "warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)" + ] + }, + { + "cell_type": "code", + "source": [ + "from pathlib import Path\n", + "\n", + "\n", + "def convert(model: torch.nn.Module, xml_path: str, **convert_kwargs) -> Path:\n", + " xml_path = Path(xml_path)\n", + " if not xml_path.exists():\n", + " xml_path.parent.mkdir(parents=True, exist_ok=True)\n", + " with torch.no_grad():\n", + " converted_model = ov.convert_model(model, **convert_kwargs)\n", + " ov.save_model(converted_model, xml_path)\n", + " del converted_model\n", + " gc.collect()\n", + " torch._C._jit_clear_class_registry()\n", + " torch.jit._recursive.concrete_type_store = torch.jit._recursive.ConcreteTypeStore()\n", + " torch.jit._state._clear_class_state()\n", + " return xml_path" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### UNet\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text-to-video generation pipeline main component is a conditional 3D UNet model that takes a noisy sample, conditional state, and a timestep and returns a sample shaped output." + ] + }, + { + "cell_type": "code", + "source": [ + "unet_xml_path = convert(\n", + " unet,\n", + " \"models/unet.xml\",\n", + " example_input={\n", + " \"sample\": torch.randn(2, 4, 2, int(sample_height // 2), int(sample_width // 2)),\n", + " \"timestep\": torch.tensor(1),\n", + " \"encoder_hidden_states\": torch.randn(2, 77, 1024),\n", + " },\n", + " input=[\n", + " (\"sample\", (2, 4, NUM_FRAMES, sample_height, sample_width)),\n", + " (\"timestep\", ()),\n", + " (\"encoder_hidden_states\", (2, 77, 1024)),\n", + " ],\n", + ")\n", + "del unet\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### VAE\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Variational autoencoder (VAE) uses UNet output to decode latents to visual representations. Our VAE model has KL loss for encoding images into latents and decoding latent representations into images. For inference, we need only decoder part." + ] + }, + { + "cell_type": "code", + "source": [ + "class VaeDecoderWrapper(torch.nn.Module):\n", + " def __init__(self, vae):\n", + " super().__init__()\n", + " self.vae = vae\n", + "\n", + " def forward(self, z: torch.FloatTensor):\n", + " return self.vae.decode(z)" + ] + }, + { + "cell_type": "code", + "source": [ + "vae_decoder_xml_path = convert(\n", + " VaeDecoderWrapper(vae),\n", + " \"models/vae.xml\",\n", + " example_input=torch.randn(2, 4, 32, 32),\n", + " input=((NUM_FRAMES, 4, sample_height, sample_width)),\n", + ")\n", + "del vae\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Text encoder\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text encoder is used to encode the input prompt to tensor. Default tensor length is 77." + ] + }, + { + "cell_type": "code", + "source": [ + "text_encoder_xml = convert(\n", + " text_encoder,\n", + " \"models/text_encoder.xml\",\n", + " example_input=torch.ones(1, 77, dtype=torch.int64),\n", + " input=((1, 77), ov.Type.i64),\n", + ")\n", + "del text_encoder\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Build a pipeline\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def tensor2vid(video: torch.Tensor, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) -> List[np.ndarray]:\n", + " # This code is copied from https://github.com/modelscope/modelscope/blob/1509fdb973e5871f37148a4b5e5964cafd43e64d/modelscope/pipelines/multi_modal/text_to_video_synthesis_pipeline.py#L78\n", + " # reshape to ncfhw\n", + " mean = torch.tensor(mean, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " std = torch.tensor(std, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " # unnormalize back to [0,1]\n", + " video = video.mul_(std).add_(mean)\n", + " video.clamp_(0, 1)\n", + " # prepare the final outputs\n", + " i, c, f, h, w = video.shape\n", + " images = video.permute(2, 3, 0, 4, 1).reshape(f, h, i * w, c) # 1st (frames, h, batch_size, w, c) 2nd (frames, h, batch_size * w, c)\n", + " images = images.unbind(dim=0) # prepare a list of indvidual (consecutive frames)\n", + " images = [(image.cpu().numpy() * 255).astype(\"uint8\") for image in images] # f h w c\n", + " return images" + ] + }, + { + "cell_type": "code", + "source": [ + "try:\n", + " from diffusers.utils import randn_tensor\n", + "except ImportError:\n", + " from diffusers.utils.torch_utils import randn_tensor\n", + "\n", + "\n", + "class OVTextToVideoSDPipeline(diffusers.DiffusionPipeline):\n", + " def __init__(\n", + " self,\n", + " vae_decoder: ov.CompiledModel,\n", + " text_encoder: ov.CompiledModel,\n", + " tokenizer: transformers.CLIPTokenizer,\n", + " unet: ov.CompiledModel,\n", + " scheduler: diffusers.schedulers.DDIMScheduler,\n", + " ):\n", + " super().__init__()\n", + "\n", + " self.vae_decoder = vae_decoder\n", + " self.text_encoder = text_encoder\n", + " self.tokenizer = tokenizer\n", + " self.unet = unet\n", + " self.scheduler = scheduler\n", + " self.vae_scale_factor = vae_scale_factor\n", + " self.unet_in_channels = unet_in_channels\n", + " self.width = WIDTH\n", + " self.height = HEIGHT\n", + " self.num_frames = NUM_FRAMES\n", + "\n", + " def __call__(\n", + " self,\n", + " prompt: Union[str, List[str]] = None,\n", + " num_inference_steps: int = 50,\n", + " guidance_scale: float = 9.0,\n", + " negative_prompt: Optional[Union[str, List[str]]] = None,\n", + " eta: float = 0.0,\n", + " generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,\n", + " latents: Optional[torch.FloatTensor] = None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " output_type: Optional[str] = \"np\",\n", + " return_dict: bool = True,\n", + " callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None,\n", + " callback_steps: int = 1,\n", + " ):\n", + " r\"\"\"\n", + " Function invoked when calling the pipeline for generation.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts to guide the video generation. If not defined, one has to pass `prompt_embeds`.\n", + " instead.\n", + " num_inference_steps (`int`, *optional*, defaults to 50):\n", + " The number of denoising steps. More denoising steps usually lead to a higher quality videos at the\n", + " expense of slower inference.\n", + " guidance_scale (`float`, *optional*, defaults to 7.5):\n", + " Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).\n", + " `guidance_scale` is defined as `w` of equation 2. of [Imagen\n", + " Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >\n", + " 1`. Higher guidance scale encourages to generate videos that are closely linked to the text `prompt`,\n", + " usually at the expense of lower video quality.\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the video generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " eta (`float`, *optional*, defaults to 0.0):\n", + " Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to\n", + " [`schedulers.DDIMScheduler`], will be ignored for others.\n", + " generator (`torch.Generator` or `List[torch.Generator]`, *optional*):\n", + " One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)\n", + " to make generation deterministic.\n", + " latents (`torch.FloatTensor`, *optional*):\n", + " Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for video\n", + " generation. Can be used to tweak the same generation with different prompts. If not provided, a latents\n", + " tensor will ge generated by sampling using the supplied random `generator`. Latents should be of shape\n", + " `(batch_size, num_channel, num_frames, height, width)`.\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " output_type (`str`, *optional*, defaults to `\"np\"`):\n", + " The output format of the generate video. Choose between `torch.FloatTensor` or `np.array`.\n", + " return_dict (`bool`, *optional*, defaults to `True`):\n", + " Whether or not to return a [`~pipelines.stable_diffusion.TextToVideoSDPipelineOutput`] instead of a\n", + " plain tuple.\n", + " callback (`Callable`, *optional*):\n", + " A function that will be called every `callback_steps` steps during inference. The function will be\n", + " called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`.\n", + " callback_steps (`int`, *optional*, defaults to 1):\n", + " The frequency at which the `callback` function will be called. If not specified, the callback will be\n", + " called at every step.\n", + "\n", + " Returns:\n", + " `List[np.ndarray]`: generated video frames\n", + " \"\"\"\n", + "\n", + " num_images_per_prompt = 1\n", + "\n", + " # 1. Check inputs. Raise error if not correct\n", + " self.check_inputs(\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt,\n", + " prompt_embeds,\n", + " negative_prompt_embeds,\n", + " )\n", + "\n", + " # 2. Define call parameters\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)\n", + " # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`\n", + " # corresponds to doing no classifier free guidance.\n", + " do_classifier_free_guidance = guidance_scale > 1.0\n", + "\n", + " # 3. Encode input prompt\n", + " prompt_embeds = self._encode_prompt(\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt,\n", + " prompt_embeds=prompt_embeds,\n", + " negative_prompt_embeds=negative_prompt_embeds,\n", + " )\n", + "\n", + " # 4. Prepare timesteps\n", + " self.scheduler.set_timesteps(num_inference_steps)\n", + " timesteps = self.scheduler.timesteps\n", + "\n", + " # 5. Prepare latent variables\n", + " num_channels_latents = self.unet_in_channels\n", + " latents = self.prepare_latents(\n", + " batch_size * num_images_per_prompt,\n", + " num_channels_latents,\n", + " prompt_embeds.dtype,\n", + " generator,\n", + " latents,\n", + " )\n", + "\n", + " # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline\n", + " extra_step_kwargs = {\"generator\": generator, \"eta\": eta}\n", + "\n", + " # 7. Denoising loop\n", + " num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order\n", + " with self.progress_bar(total=num_inference_steps) as progress_bar:\n", + " for i, t in enumerate(timesteps):\n", + " # expand the latents if we are doing classifier free guidance\n", + " latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents\n", + " latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)\n", + "\n", + " # predict the noise residual\n", + " noise_pred = self.unet(\n", + " {\n", + " \"sample\": latent_model_input,\n", + " \"timestep\": t,\n", + " \"encoder_hidden_states\": prompt_embeds,\n", + " }\n", + " )[0]\n", + " noise_pred = torch.tensor(noise_pred)\n", + "\n", + " # perform guidance\n", + " if do_classifier_free_guidance:\n", + " noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)\n", + " noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)\n", + "\n", + " # reshape latents\n", + " bsz, channel, frames, width, height = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + " noise_pred = noise_pred.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + "\n", + " # compute the previous noisy sample x_t -> x_t-1\n", + " latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample\n", + "\n", + " # reshape latents back\n", + " latents = latents[None, :].reshape(bsz, frames, channel, width, height).permute(0, 2, 1, 3, 4)\n", + "\n", + " # call the callback, if provided\n", + " if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):\n", + " progress_bar.update()\n", + " if callback is not None and i % callback_steps == 0:\n", + " callback(i, t, latents)\n", + "\n", + " video_tensor = self.decode_latents(latents)\n", + "\n", + " if output_type == \"pt\":\n", + " video = video_tensor\n", + " else:\n", + " video = tensor2vid(video_tensor)\n", + "\n", + " if not return_dict:\n", + " return (video,)\n", + "\n", + " return {\"frames\": video}\n", + "\n", + " # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt\n", + " def _encode_prompt(\n", + " self,\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt=None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " ):\n", + " r\"\"\"\n", + " Encodes the prompt into text encoder hidden states.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " prompt to be encoded\n", + " num_images_per_prompt (`int`):\n", + " number of images that should be generated per prompt\n", + " do_classifier_free_guidance (`bool`):\n", + " whether to use classifier free guidance or not\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the image generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " \"\"\"\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " if prompt_embeds is None:\n", + " text_inputs = self.tokenizer(\n", + " prompt,\n", + " padding=\"max_length\",\n", + " max_length=self.tokenizer.model_max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + " text_input_ids = text_inputs.input_ids\n", + " untruncated_ids = self.tokenizer(prompt, padding=\"longest\", return_tensors=\"pt\").input_ids\n", + "\n", + " if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):\n", + " removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1])\n", + " print(\n", + " \"The following part of your input was truncated because CLIP can only handle sequences up to\"\n", + " f\" {self.tokenizer.model_max_length} tokens: {removed_text}\"\n", + " )\n", + "\n", + " prompt_embeds = self.text_encoder(text_input_ids)\n", + " prompt_embeds = prompt_embeds[0]\n", + " prompt_embeds = torch.tensor(prompt_embeds)\n", + "\n", + " bs_embed, seq_len, _ = prompt_embeds.shape\n", + " # duplicate text embeddings for each generation per prompt, using mps friendly method\n", + " prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # get unconditional embeddings for classifier free guidance\n", + " if do_classifier_free_guidance and negative_prompt_embeds is None:\n", + " uncond_tokens: List[str]\n", + " if negative_prompt is None:\n", + " uncond_tokens = [\"\"] * batch_size\n", + " elif type(prompt) is not type(negative_prompt):\n", + " raise TypeError(f\"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=\" f\" {type(prompt)}.\")\n", + " elif isinstance(negative_prompt, str):\n", + " uncond_tokens = [negative_prompt]\n", + " elif batch_size != len(negative_prompt):\n", + " raise ValueError(\n", + " f\"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:\"\n", + " f\" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches\"\n", + " \" the batch size of `prompt`.\"\n", + " )\n", + " else:\n", + " uncond_tokens = negative_prompt\n", + "\n", + " max_length = prompt_embeds.shape[1]\n", + " uncond_input = self.tokenizer(\n", + " uncond_tokens,\n", + " padding=\"max_length\",\n", + " max_length=max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + "\n", + " negative_prompt_embeds = self.text_encoder(uncond_input.input_ids)\n", + " negative_prompt_embeds = negative_prompt_embeds[0]\n", + " negative_prompt_embeds = torch.tensor(negative_prompt_embeds)\n", + "\n", + " if do_classifier_free_guidance:\n", + " # duplicate unconditional embeddings for each generation per prompt, using mps friendly method\n", + " seq_len = negative_prompt_embeds.shape[1]\n", + "\n", + " negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # For classifier free guidance, we need to do two forward passes.\n", + " # Here we concatenate the unconditional and text embeddings into a single batch\n", + " # to avoid doing two forward passes\n", + " prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])\n", + "\n", + " return prompt_embeds\n", + "\n", + " def prepare_latents(\n", + " self,\n", + " batch_size,\n", + " num_channels_latents,\n", + " dtype,\n", + " generator,\n", + " latents=None,\n", + " ):\n", + " shape = (\n", + " batch_size,\n", + " num_channels_latents,\n", + " self.num_frames,\n", + " self.height // self.vae_scale_factor,\n", + " self.width // self.vae_scale_factor,\n", + " )\n", + " if isinstance(generator, list) and len(generator) != batch_size:\n", + " raise ValueError(\n", + " f\"You have passed a list of generators of length {len(generator)}, but requested an effective batch\"\n", + " f\" size of {batch_size}. Make sure the batch size matches the length of the generators.\"\n", + " )\n", + "\n", + " if latents is None:\n", + " latents = randn_tensor(shape, generator=generator, dtype=dtype)\n", + "\n", + " # scale the initial noise by the standard deviation required by the scheduler\n", + " latents = latents * self.scheduler.init_noise_sigma\n", + " return latents\n", + "\n", + " def check_inputs(\n", + " self,\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt=None,\n", + " prompt_embeds=None,\n", + " negative_prompt_embeds=None,\n", + " ):\n", + " if self.height % 8 != 0 or self.width % 8 != 0:\n", + " raise ValueError(f\"`height` and `width` have to be divisible by 8 but are {self.height} and {self.width}.\")\n", + "\n", + " if (callback_steps is None) or (callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0)):\n", + " raise ValueError(f\"`callback_steps` has to be a positive integer but is {callback_steps} of type\" f\" {type(callback_steps)}.\")\n", + "\n", + " if prompt is not None and prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to\" \" only forward one of the two.\"\n", + " )\n", + " elif prompt is None and prompt_embeds is None:\n", + " raise ValueError(\"Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined.\")\n", + " elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):\n", + " raise ValueError(f\"`prompt` has to be of type `str` or `list` but is {type(prompt)}\")\n", + "\n", + " if negative_prompt is not None and negative_prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:\"\n", + " f\" {negative_prompt_embeds}. Please make sure to only forward one of the two.\"\n", + " )\n", + "\n", + " if prompt_embeds is not None and negative_prompt_embeds is not None:\n", + " if prompt_embeds.shape != negative_prompt_embeds.shape:\n", + " raise ValueError(\n", + " \"`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but\"\n", + " f\" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`\"\n", + " f\" {negative_prompt_embeds.shape}.\"\n", + " )\n", + "\n", + " def decode_latents(self, latents):\n", + " scale_factor = 0.18215\n", + " latents = 1 / scale_factor * latents\n", + "\n", + " batch_size, channels, num_frames, height, width = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(batch_size * num_frames, channels, height, width)\n", + " image = self.vae_decoder(latents)[0]\n", + " image = torch.tensor(image)\n", + " video = (\n", + " image[None, :]\n", + " .reshape(\n", + " (\n", + " batch_size,\n", + " num_frames,\n", + " -1,\n", + " )\n", + " + image.shape[2:]\n", + " )\n", + " .permute(0, 2, 1, 3, 4)\n", + " )\n", + " # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16\n", + " video = video.float()\n", + " return video" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Inference with OpenVINO\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "device = widgets.Dropdown(\n", + " options=core.available_devices + [\"AUTO\"],\n", + " value=\"AUTO\",\n", + " description=\"Device:\",\n", + " disabled=False,\n", + ")\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_unet = core.compile_model(unet_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_vae_decoder = core.compile_model(vae_decoder_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_text_encoder = core.compile_model(text_encoder_xml, device_name=device.value)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Here we replace the pipeline parts with versions converted to OpenVINO IR and compiled to specific device. Note that we use original pipeline tokenizer and scheduler." + ] + }, + { + "cell_type": "code", + "source": [ + "ov_pipe = OVTextToVideoSDPipeline(ov_vae_decoder, ov_text_encoder, tokenizer, ov_unet, scheduler)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Define a prompt\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "prompt = \"A panda eating bamboo on a rock.\"" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Let's generate a video for our prompt. For full list of arguments, see `__call__` function definition of `OVTextToVideoSDPipeline` class in [Build a pipeline](#Build-a-pipeline) section." + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Video generation\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "frames = ov_pipe(prompt, num_inference_steps=25)[\"frames\"]" + ] + }, + { + "cell_type": "code", + "source": [ + "images = [PIL.Image.fromarray(frame) for frame in frames]\n", + "images[0].save(\"output.gif\", save_all=True, append_images=images[1:], duration=125, loop=0)\n", + "with open(\"output.gif\", \"rb\") as gif_file:\n", + " b64 = f\"data:image/gif;base64,{base64.b64encode(gif_file.read()).decode()}\"\n", + "IPython.display.HTML(f'')" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Interactive demo\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def generate(prompt, seed, num_inference_steps, _=gr.Progress(track_tqdm=True)):\n", + " generator = torch.Generator().manual_seed(seed)\n", + " frames = ov_pipe(\n", + " prompt,\n", + " num_inference_steps=num_inference_steps,\n", + " generator=generator,\n", + " )[\"frames\"]\n", + " out_file = tempfile.NamedTemporaryFile(suffix=\".gif\", delete=False)\n", + " images = [PIL.Image.fromarray(frame) for frame in frames]\n", + " images[0].save(out_file, save_all=True, append_images=images[1:], duration=125, loop=0)\n", + " return out_file.name\n", + "\n", + "\n", + "demo = gr.Interface(\n", + " generate,\n", + " [\n", + " gr.Textbox(label=\"Prompt\"),\n", + " gr.Slider(0, 1000000, value=42, label=\"Seed\", step=1),\n", + " gr.Slider(10, 50, value=25, label=\"Number of inference steps\", step=1),\n", + " ],\n", + " gr.Image(label=\"Result\"),\n", + " examples=[\n", + " [\"An astronaut riding a horse.\", 0, 25],\n", + " [\"A panda eating bamboo on a rock.\", 0, 25],\n", + " [\"Spiderman is surfing.\", 0, 25],\n", + " ],\n", + " allow_flagging=\"never\",\n", + ")\n", + "\n", + "try:\n", + " demo.queue().launch(debug=True)\n", + "except Exception:\n", + " demo.queue().launch(share=True, debug=True)\n", + "# if you are launching remotely, specify server_name and server_port\n", + "# demo.launch(server_name='your server name', server_port='server port in int')\n", + "# Read more in the docs: https://gradio.app/docs/" + ] + } + ] + .map(fromJupyterCell) + , + [ + { + "cell_type": "markdown", + "source": [ + "# Video generation with ZeroScope and OpenVINO\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Install and import required packages](#Install-and-import-required-packages)\n", + "- [Load the model](#Load-the-model)\n", + "- [Convert the model](#Convert-the-model)\n", + " - [Define the conversion function](#Define-the-conversion-function)\n", + " - [UNet](#UNet)\n", + " - [VAE](#VAE)\n", + " - [Text encoder](#Text-encoder)\n", + "- [Build a pipeline](#Build-a-pipeline)\n", + "- [Inference with OpenVINO](#Inference-with-OpenVINO)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Define a prompt](#Define-a-prompt)\n", + " - [Video generation](#Video-generation)\n", + "- [Interactive demo](#Interactive-demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The ZeroScope model is a free and open-source text-to-video model that can generate realistic and engaging videos from text descriptions. It is based on the [Modelscope](https://modelscope.cn/models/damo/text-to-video-synthesis/summary) model, but it has been improved to produce higher-quality videos with a 16:9 aspect ratio and no Shutterstock watermark. The ZeroScope model is available in two versions: ZeroScope_v2 576w, which is optimized for rapid content creation at a resolution of 576x320 pixels, and ZeroScope_v2 XL, which upscales videos to a high-definition resolution of 1024x576.\n", + "\n", + "The ZeroScope model is trained on a dataset of over 9,000 videos and 29,000 tagged frames. It uses a diffusion model to generate videos, which means that it starts with a random noise image and gradually adds detail to it until it matches the text description. The ZeroScope model is still under development, but it has already been used to create some impressive videos. For example, it has been used to create videos of people dancing, playing sports, and even driving cars.\n", + "\n", + "The ZeroScope model is a powerful tool that can be used to create various videos, from simple animations to complex scenes. It is still under development, but it has the potential to revolutionize the way we create and consume video content.\n", + "\n", + "Both versions of the ZeroScope model are available on Hugging Face:\n", + " - [ZeroScope_v2 576w](https://huggingface.co/cerspense/zeroscope_v2_576w)\n", + " - [ZeroScope_v2 XL](https://huggingface.co/cerspense/zeroscope_v2_XL)\n", + "\n", + "We will use the first one." + ] + }, + { + "cell_type": "markdown", + "source": [ + "
\n", + " This tutorial requires at least 24GB of free memory to generate a video with a frame size of 432x240 and 16 frames. Increasing either of these values will require more memory and take more time.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Install and import required packages\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "To work with text-to-video synthesis model, we will use Hugging Face's [Diffusers](https://github.com/huggingface/diffusers) library. It provides already pretrained model from `cerspense`." + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install -q --extra-index-url https://download.pytorch.org/whl/cpu \"diffusers>=0.18.0\" \"torch>=2.1\" transformers \"openvino>=2023.1.0\" numpy \"gradio>=4.19\"" + ] + }, + { + "cell_type": "code", + "source": [ + "import gc\n", + "from typing import Optional, Union, List, Callable\n", + "import base64\n", + "import tempfile\n", + "import warnings\n", + "\n", + "import diffusers\n", + "import transformers\n", + "import numpy as np\n", + "import IPython\n", + "import torch\n", + "import PIL\n", + "import gradio as gr\n", + "\n", + "import openvino as ov" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Original 576x320 inference requires a lot of RAM (>100GB), so let's run our example on a smaller frame size, keeping the same aspect ratio. Try reducing values below to reduce the memory consumption." + ] + }, + { + "cell_type": "code", + "source": [ + "WIDTH = 432 # must be divisible by 8\n", + "HEIGHT = 240 # must be divisible by 8\n", + "NUM_FRAMES = 16" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The model is loaded from HuggingFace using `.from_pretrained` method of `diffusers.DiffusionPipeline`." + ] + }, + { + "cell_type": "code", + "source": [ + "pipe = diffusers.DiffusionPipeline.from_pretrained(\"cerspense/zeroscope_v2_576w\")" + ] + }, + { + "cell_type": "code", + "source": [ + "unet = pipe.unet\n", + "unet.eval()\n", + "vae = pipe.vae\n", + "vae.eval()\n", + "text_encoder = pipe.text_encoder\n", + "text_encoder.eval()\n", + "tokenizer = pipe.tokenizer\n", + "scheduler = pipe.scheduler\n", + "vae_scale_factor = pipe.vae_scale_factor\n", + "unet_in_channels = pipe.unet.config.in_channels\n", + "sample_width = WIDTH // vae_scale_factor\n", + "sample_height = HEIGHT // vae_scale_factor\n", + "del pipe\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Convert the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The architecture for generating videos from text comprises three distinct sub-networks: one for extracting text features, another for translating text features into the video latent space using a diffusion model, and a final one for mapping the video latent space to the visual space. The collective parameters of the entire model amount to approximately 1.7 billion. It's capable of processing English input. The diffusion model is built upon the Unet3D model and achieves video generation by iteratively denoising a starting point of pure Gaussian noise video." + ] + }, + { + "cell_type": "markdown", + "source": [ + "![](data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCADoA/0DASIAAhEBAxEB/8QAHQABAQACAwEBAQAAAAAAAAAAAAcFBgMECAIJAf/EAFoQAAEDAwICAwsJBAcFBgMHBQIBAwQABQYHERIhCBMYFBUiMUFWWJaX09UWMjhRVXeVtdQXI2FxCTdCUoGRtjM0NnWhJCVidrG0NZTwQ2V0goOS4ZPBw9Hx/8QAGgEBAAMBAQEAAAAAAAAAAAAAAAMEBQECBv/EADwRAQABAgIHBQYDBwQDAAAAAAABAgMEERITITFSkdEUQVFhsQUicYHB0jIz8EJTVJKh4fEjNHKyFTWC/9oADAMBAAIRAxEAPwD1n0ZOjJ0bb/0bdKL7fej5prcblccHsUuZMl4nAefkvuQGScdccJpSMyJVJSVVVVVVWulfcG6OFk1Hj6aB/R3Wy4TZrb8mHNi4tiCRJMVk2wdkCrswHBAVeb8E2xcXfkC7LVY6J30WNG/u/wAe/LmK5M5wXJsh1Zs1+tjKs21nEb7aHZ6Oinc8qS5EVnweJDXk04u4psnDzVFVNw1OzaQ9AvInJzNg0u0DuTlrnhap4Q7JZniiTiPgGM6gAvA8pooo2WxKqbbb1j7vpv0DrLn2P6aTNFdFu/2SOzGIUUccs3H10YANxogUUPj2NNhQVXx77VqOmOiuqdpgP2zIsby+bJiYwxhjD+RXLHkgACvBvIit2thqQ5Ga6vrgKQ4EjdRFGhIjcGl4/hec41kWnEV7FJtwi4tIu9snXVqbHJZDUhoCbuTqOvI6SuEKo6iITnWkSoJAvHQde+aK9BXGLvEsGS6S6EWm6XB5qPEhTrDZ2JEh13i6oG2zbQjI+A+FERVLgLbfZa45uj3QNt0u8wLhpboHFlY7GKbeGHrJZgct0cURSekCobsgiKm5HsibpzrXNVrFkuSav6j4vi+n3fuXk2D2iyJdEfjNhaOtfnKj8hHnANWUVEP9wLjnG2Pgf2kzN90lzJjG8zkwsWC7znc9tmUswFlMAd6jQ24O4iThdWDhLHNAR0gTjAeIgFeKg/svTH+j7t+NRMzn6e9HqNj89kpMW7PWmyBDkNCQiTjbyhwGKEYipIqoikieVK7MPR/oGXHvolv0u0Dk943WWbn1Nksx9wuPbdUD+wfuiPiThQtlLdNt96xeH6TZrMz4dQb7gyWeNc5eR3Ru1Pyozr1qOXGhMNC71TptK871Ehw+pIwRXF3JVXddKzPSXIMO0itUKXhMBqNBwjGMcfg9YwjBzW7syRxSQFVOFeJdyRFDwl2VaDap+J/0esNcUci6V6FXOLmd4csVqlwLHY3o7ssAMjDjRNi2Jvq1QOIkMwFU51Ruyd0WPRp0q9Tbd7mtJhYbqPcsvg6nPaZXCztTM4iXN2wHMt6zocRu1OwTlPq1IKMRKZiai06Z9UI8lP8Adp6LoJV0TvosaN/d/j35cxVVqVdE76LGjf3f49+XMVVaBSlKCL9IrGMazTJdFsTzHHrZfrHcs+kBNtlziNyokkQxq9ugjjLiKBoLjbZpxIuxAJJzRFrJ9k7osejTpV6m273NNZP6xdCfvAmf6Vv1VWglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCWdFR12R0XtHn33TcdcwHHzMzJVIiW3MKqqq+NVWqnUq6J30WNG/u/x78uYqq0ClKUClKUClKUClKUClKUClKUClKUClKUEq6J30WNG/u/x78uYqq1Kuid9FjRv7v8AHvy5iqrQYIcmORf7vYbfbikHZ4TMh13rOESfd41BhOS8+EEJV8iGHJd60Sw6r6mXPBcgyi9aODYblZ7FFu0aBPvDoNSn3IqvPRie7k4m+qJOrUuqJVXxgPircLLYJtozrI7qMcSgX5qHK67jTiGS0CsmCjvvt1YMqionj49/JXczmxXLKMMveNWi5RLfMusB+E1Klwylssq4ChxGyLjauIiKvgo4O/10Gq2bU/IbZartK1YxCLZZlqjxppM4xKm5GLzD5m22gC3CZkG7xtkitiwXJRVCLdUHsXLXHTu1WK05FLk34o17N1mExHxm5vzDebRVcYKI3HJ9t9EE1Vk2xc8A/B8EttWj9HKz27R57TSw2XTWyyZr4SrkFswYY1huboqibSrWElCeAgEEISkbqoDuvCnBXPgmimVYDbsastnyrEItssF5m3MoVuw8oMdWpDLg9zx2m5nBHEDecJF4T3HgFUUkJwg2XJtZsJx2w229pLmzVvbBP25qLaZ8oiFB362QEaO67FYFVEXHnW0BtSRC2XYV60TW/FGrFid1yFm4xHMrt7U9s7fa51zgRRMQVSenMR1ZYaRTTZ19WhJN15Ii7Ye0aV6qY9ZLYzZNTsZavTPdMa4zHsSfcjSYbkhx4Baj93oTLwK4SI4TrgLuqq0vJB1PLeitdsrs2N2e45VhVzLH7S3bGpl9wZLlIjOMkStSoClLEYTyooI6aCamrYKPVbJsHoilcccXxYbGU4248gIjhtgoCRbc1QVVVFFXxIqrt9a+OuSglXRO+ixo393+PflzFVWpV0TvosaN/d/j35cxVVoFKUoJVrJ/WLoT94Ez/St+qq1KtZP6xdCfvAmf6Vv1VWgVq94zN+PcHbVj9oG5SIqoMlx2T3OwyaoioCmgmSnsqKqIKoiKm6pvtW0VNcf5s3FxeZFebpxL5V2mvCn+SIif4Vje1cTetVUWbNWjNWczOyZyjLZGcTG2ZjfE7InvnONDAWLd2aqrkZxGWz4/D4Mp8r80807J+OO/pafK/NPNOyfjjv6WlKytbjf4irlb+xp9mw37uOdX3HyvzTzTsn447+lp8r80807J+OO/paUprcb/ABFXK39h2bDfu451fcfK/NPNOyfjjv6WnyvzTzTsn447+lpSmtxv8RVyt/Ydmw37uOdX3HyvzTzTsn447+lp8r80807J+OO/paUprcb/ABFXK39h2bDfu451fc/qZvk0ZetuOIRSjjzPuC5k+8ieVUA2W0L+SFv9SLW2wJ0S5wmLjAfF+NJbF1pwfEQqm6LWo13tNuWKoCeILlcwFPqEZz6In8kRESrvs3F4jtXZ7tc1xNMztiImMppj9mI36XfHcp47DWqLWst05bYjv74nxmfBtFY+/X2345bHbrcjNGm1EBBseJx1wlQQbAf7RESoiJ9a1kK0fU/m9iTa8xO+rxJ5F2gSyT/IhRf5olfT2LcXbkUzuYl2uaKJqh11z7NnV442D2oG1+aMq+mDqJ/4hbjGKL/I1/nX8+XWe+Zdg9YXv0dfNK1ez2OCOc9WfrrvF6dH18us98y7B6wvfo6fLrPfMuwesL36OvmlOz2OCOc9TXXeL06Pr5dZ75l2D1he/R0+XWe+Zdg9YXv0dfNKdnscEc56muu8Xp0fXy6z3zLsHrC9+jp8us98y7B6wvfo6+aU7PY4I5z1Ndd4vTo+vl1nvmXYPWF79HT5dZ75l2D1he/R180p2exwRznqa67xenR9fLrPfMuwesL36Ony6z3zLsHrC9+jr5pTs9jgjnPU113i9Oj6+XWe+Zdg9YXv0dPl1nvmXYPWF79HXzSnZ7HBHOeprrvF6dGbxnNu/M4rLeLUVquiNq8211yPNSG0VEImnEQeLhVU3QhEk3Rdtl3raKlx+DmWHGPIiukhtVTyitvlqqfy3EV/wSqjWfjLVNquNDdMZ/1mPouYe5VcpnS7pKUpVRYKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQSronfRY0b+7/Hvy5iqrUq6J30WNG/u/x78uYqq0ClKUClKUClKUClKUClKUClKUClKUClKUEq6J30WNG/u/x78uYqq1Kuid9FjRv7v8e/LmKqtBJNbOk5ppoNcrXac1ltJKubZSeArzaoCsxhLhJ7/vCXHV5EXfwGOtc5fM5pv95X0ltOcQy7EMUuDquBmzDEi13AbnbWWzB4uFrhjvym5j/Eqj/u8d3h4kUuFN1TZ830+n5VLj3TH9R8mwq4tN9zvS7E3b3DlMoqkLbgToslvYSUlRRASTiJOLZVSsPlWix5RcSnjqnmdpGQzFGexb+9whOfjKhMyHCciGYkhCiqDZAyXNCbVCJFDsBqhkrmp8zTxvRfLXbfDGKZZI3NtPcCA8jn7wmymJKQEVtR5MkaqheAg8JFmbLltyueaZBjMvHLjbmbRHjOxnZIRlCeLiuorzJtSXC4N2+DgdaZNFBV8ISRU69607cu+XQ8qYzrJbW002y1OtdvcjNxbojJGbXXmrCyB4ScJdmXmkJPBNDFVFehG0uvsfUV/UBdZs0dZkCjR2E41n739QPWK2yhJASVwiTpEi9fxquyERJyoOrjGt8C/SXhvGB5Vi8IIsuUzPvAQ+pk9ylwyQbGPIdd4m1RV3IBE05tkac67GI6uP5Tlw4dL0uzPHpBwHbmMm7NwUjlGFwQbJCZlOFxOKSqgcPGCAvWi0pAh9WFogyx3kbuWpGWXWNaG7qy7HlhbRC4NzyVXAfVmIBIgIuwK0Ta7fOU151jNIdOdTcey+95NqVkr103ZW22dHLyzPIYXW8Y8Qs22CDKpsnJUfNd14niQR2CvUpSglXRO+ixo393+PflzFVWvL/Rk6MnRtv/AEbdKL7fej5prcblccHsUuZMl4nAefkvuQGScdccJpSMyJVJSVVVVVVWqX2Tuix6NOlXqbbvc0FVpUq7J3RY9GnSr1Nt3uadk7osejTpV6m273NA1k/rF0J+8CZ/pW/VVa8v6sdGTo227PNGIdv6PmmsWPdM4lRJzTOJwACUwmN3p5GnRRrYwR1lpxBLdONsC8YoqUvsndFj0adKvU23e5oNRs/Th0NLWDItB8+uz2BZlYbkcFuNkKgxHuTSqhMSI8hCVvhebNoxBxQNesREEtt63nHVQos8hVFRbzdVRU//ABz9ecbN/RYaF3LWDItWdTYNtuUSdcjes2JWG2hZ7Lb4YKgxwcaYVFecQAAjVFbAjJziA9969D4dAg2q0PWu1w2YkOHc7lHjx2G0BtloJrwiACnIRREREROSIlfP+1/9za/41+tDW9mfhr+X1Zl1HVaNGTEHFFeAiHiFC8iqiKm6fw3T+dQSBqPrHjWV5aOomZ4VPsGFdwLJZsuFzWJ1zKW0qtNMEd0dFs+tVsE4gNC358HjS+1Nck0XZyORm8lzIjjuZaVrkRiCIJLb5MBEJlzwiVHU6wQJRVB5Iqb890q0TEZxUv1xM7YYWV0iYDUm0JcbJMxZW72/a8jgX9ptZdvEYD0pshWK86yfWIDSirZuIqEo7IaKg5S+6+2fHnhauGC5YvcsNq5XtW2Iq94oTrhg0/LHujiVC6sy4GEdcERVTAaxLfR4dvsxq96mZTAyS6SLwd0uaM2ZYsN4O4HYTTDDBvukwgA5x8ZOOEp8SpwoqIOEyzonQsuvFuv94umI3m5s2+NaZ1yyPCIt4mlGjuGTLkVx9zhjyOBxRMzB5syRD6oV3RZIi1ntlH/q5NwLpAWD5aP4g3h+UnHi3sMckXxI0fvezPcjg+02u76PkJi6CIYtECEvCRCtY9rpAS4lqxOfI0xy29Rcp7kZj3a3Ba2GDffJU4EivXFZW4ChGaADiCAkXEqCW2ad0dEzlKGQIAycti5SgpDTwEZaab7n5Gic+q349k24tuHlWsY5ohqhh95s0yxao4rJgWW2x7XFYu+HyJL0Zof94WO61cWhbJ5UTcibMk4RTdRThrkavL/Lv+p+smyQdeManZAdoHHcgbtzj06JAvhssLAuMqGhrJjs8LqvCY9U7srrTYH1ZcBFy32TTjOmdScRgZpDxy72eFdGgkwm7ojCPPRzFCB3hZdcQRJC5ISoaeUUqXY50VMexnPJ+WWwMNYjyHrjLaeZwqIN8J+ZxqaP3RSI3WxJ01FAbac2QRJw0RUKv4ZjvyRxCyYp3Z3X3mt8eB1/V9X1vVNiHHw7rw78O+267b+Na816ER7r1RpzPvMxXe02/wCF1/5pdf8A379dGtNx/o9aBZzEl5Tmuh+n+QXqbdLj3TcbpjMKXKf4JjwBxuuNkZcIAIpuvIRRE5Ild9n/APsaP+Ff/a2hx/8Atf8A6j0qZvpC68WLo44E1qZluOXm548xco8K7P2tttw7Yw9xCMpwDIeJtHeqbVBXi3dRURdtq16NrXpTrlZsOyrSfOrXklv7+r1qxHf3scits1UF5ktnGSVOfC4Irtz2rS+kL0BNGtW8CawbT3T7TjTx2Zco7l0vdtwuF3eMBviM2orjYtk06bgsop8WyB1iKhb7L9af9ETQ/otQ8Wh6W404Fym3hWbhep73Xz5ojb5i7GeyCA7oi8DYgG6IvDvzr7HCfnR8/SXzWJ/Ln5erfNQcvbwPDLtlpwHJxW9jiZiNlwlIeIkBtpC2Xh4jIR325b7+StRcvetuI2i7XXMkxS9MNWabcWn7TAehDbZDLXGDDwOynSlAfhJ1rfVKnBzBOPcd2zLFLXnOLXPEr11qQ7pHJhw2S4XG9+YmBbLsYkiEK7clRFrS2tN9SrxFuELPdVotzYdtMu1w2bXYjt7e77fAsiWJSXu6XRT5vArLaKRrwbqKhqVZ57FCMu927vqFeoGm2K5izFhLNvkmxMyGyA+qEZrzAO8CcW6KiOlw7quyom+/llFw6R2SRswye1NapaXJMsWQu2mFgpW90shubQE2go2aXBF6xxDVRJIhCm26oqIqpv1p0o1Ldxe14fmuo2M3G32R+0vQSteKyID3/YX2nNnScuD4nxi0g+CIcKrvz+bW64RhnyN7/wD/AHl3Z38vkq9f7Hq+p67h/dfOXi24fnct9/Elecqp8nrOmGpa8ZJqrhtii5Lp9f8AFIcdJkG3yYt5x+TPMzlS2mEcBxmawgICO8SioFxKnzh3pb9Yzst4axDLIcy8yYk9mzXXJrRawh2ePcn0EmYysuy3ZIkQuMpxCjjaE4KEYqqom2aj4V+0DGkx3vn3BtcIE/rup63/AHaU1I4OHiH53VcO+/Li32XbZZ1dujJYJ+rTupsZjDUKZcmLvLdnYZFnXhJDTYAIR7i6W7DKo0CqPVGaLx8Dje6cPaoqic6XI0ZjKWQznXoLTg+U3nDsYuFzv2NWuRMm25xI6Lbn2y4RZlIT4IilsRogGqE2CkJKhBx1K0zH7ha4k+VAegvSGAdcjPKCuMkQoqgStkQKqKuy8JEnLkq+OptdNFJ9yseQW5c2cGZl1tlRL7IOGRtyJLg7MyGmld/co0i8CAhLxNoAkW4odUm1sTo1tixrnKYky2mQB95hhWW3HEREIhbIzUBVd1QVIlTxbr467TpZ7XJyy2IBgGu95zHN3bFP150dtklvIZ1qHEDtxrfHGWJLjYihrdEXrTAENF7mVPC+aqVu2OdIbGMgvCQXcWya0210ro3HvVxjxwhSHLc4YSgHgeJ4eHqzJCNsQIUVRJfFXHhmm2rmDTX4Fp1KxB7Gn7zMuhQ5OHyinI3Jkm+40kobkLfEiuKiH1G3i3Fa5w0NhO2u0Wa5X4pES3O3wnxGKgFIbuXXIQIqkvAoI+vPYt+HxJvXmmK4h6nRfY66xBsbt6maa5tEN5+KxZ4bsSKr177pVeoKMQyFaDiQSJRkOMm2KcTggioq9ST0g7S9Z4rlhwrKJ9+krObfsYRo/ddrKGojJKVxPi1wtkbaKjTjhOIYq0jm6VxFo/qLMx+Nbrvqvb359gkwpONPM4yLUaGUZCESlNLIJySbgGQOKDzAqmygDa7qvXj6D5NbkiX20ajRGstecuK3q6yLF10eY1ONsnwYjdeKx+DqWkaUnHUFA8NHd13e+e6+bL0joY4zjtyv2K3ia/JsVuvGRzbRHaWDYhlgnATwuvI8oKXGuzIvEIgpHwpsq71h+okfNL5kFpt2NXmPFx6a5bnbnJ7nGLJkguxtsoLxOrwookpE2IqhJsqqhIkmufRBsU6dj81ZGHznbdaLbZ58u+4TFu00ghpsDkF14+GGZIpISGD4fNVBFUVSsmF4imIM3dkZ/dSXS8S7qn7nq+q64uLq/Gu/Dttvy3+pK7Tp5+85Vo9zKO/8YYZ/zd/8tmVUai2Y4biGe3XEsZzrFLPkdnkXlw3rfdoLUyM4QW+YQqTTokKqhIioqpyVEWsp2Tuix6NOlXqbbvc1S9ofjp+H1lawf4avj9IVWlSrsndFj0adKvU23e5p2Tuix6NOlXqbbvc1QW1VpUq7J3RY9GnSr1Nt3uadk7osejTpV6m273NBVaVBcF0z030y6Tc236b6fY1ikWZgjb0lix2liA284lwJEMxZAUIkTluvPar1QKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQSronfRY0b+7/Hvy5iqrUq6J30WNG/u/x78uYqq0ClKUClKUClKUClKUClKUClKUClKUClKUEo6KKGvRV0dRshE10+x7hUk3RF72sbbpum/+dNLNZ7nlrMCDk+IXiK4+/KtyZAFvCLaZ06O86240w0chyU3/ALA1QnQ6ottgdc3Hd0URU+iro6AmQKWn2PIhDtuP/drHNN90/wA0ruYbogziMiMMjUvM7/bIZPPx7VdXoSx25TqmrklDZjNvcSq65sCuKyHF4DYcIcIYHHelXgWUBmrdos9ykz8ERxy426HcrRPlmy26TZuI3Emu9SqKJL1UhWXl4VRG1JFSmS6+3JvHbtcbTh16x+VZEYlvFfLa1LCSy3Laamx2WocpXVfAXBRC2UOJwOBHuEwTtNdHZxq3zrYutWoDjEm0OWKMJDZ9oEIiBRbZFICCqggcKG4hmqEvGRqgKPcyPQOFkseRFk6k5lECW884+sM4DRmDr0d5xtD7lUgRXIyLxAouD1rnCY7BwBkGtYUS0zH7hpxl1vvkdxtuNjj7cIrhP61T6lWSbklG2PqnV3N8OrQCV3q9q/j2sRDZGpEPTLMJmRnIKM5ijSQBuTBgAOGpuOShhIAg42XGklQXjERUjXhr+s6QPpaZjM7VHL59+kONuRskkBbUuEFG+PqwZAIYxeEUdeTw2DUkcLjUuW3y5o/MWyNRourGYxciGQch3KmmbUtyf4wACA2zhFD4FBpodhjj/sxVNi3JQ3HGsgiZPZmLxEjyYyOKbbseU2gPR3mzUHGnBRVRCExIV2VRXbcVVFRV17MtTCxS6BbIGBZRkysthIub1mbiqFrjkqojzySH2ic5Aa9XHF53YPmeEKFsGM2BnGLKxZ2p8qcTam49Ml9X18p4zU3HnOrEA4iMiJUARFN9hFERETXcy00m5Vd27tadSspxRHGRj3GPZgtxNXJoVVRF5ZcV8w2QjFCZJotjXdVVAUQx9y1qiW7JlsYYDlUu3NLCKRf2Bhd7mGZfJh4lKSLxCp7gqA0RiqcRCgKJrR6nGQ6Lt30b6xG1Gyqzxb03bGW40Bu29XbwhHxAkfrojheH4j61XOXzODx1RQFQAQIyNRREUi23L+K7bJ/klBLOid9FjRv7v8e/LmKqtSronfRY0b+7/Hvy5iqrQKUpQSrWT+sXQn7wJn+lb9VVqVayf1i6E/eBM/0rfqqtAqa49/u9wT6rzdf/AHz9UqtOuuJ3qLcZNxxdyC43NcV5+HMcNoRdVNiMHAElHi23UVFee6oqbrWJ7XsXKqrd+3TNUU5xMRvynKc8u/LR3Rt2tL2feotzVRXOWeX9P8uKlcPenUH7Ex78Zf8A0tO9OoP2Jj34y/8ApaydOrgr/kr+1p6y3xRzjq5qVw96dQfsTHvxl/8AS0706g/YmPfjL/6WmnVwV/yV/aay3xRzjq5qVw96dQfsTHvxl/8AS0706g/YmPfjL/6WmnVwV/yV/aay3xRzjq5qVw96dQfsTHvxl/8AS0706g/YmPfjL/6WmnVwV/yV/aay3xRzjq5q72m3/Cyr9d0uip/8+/WLSwZ9LXqHWrFbgLkUhqY7KME+sWyZbRV+rctv4L4q2+z2mHY7ZHtMASRiMHAKmW5EvjUiXykqqqqvlVVq77MsXa8XGImmYpimqNsTGczNM7Inbs0ds5d8ZZ7cqWPv25s6umc5mYnZt3RPV3a0fU//AHjEF8iX4/y6ZW8ViMoxyPk9qW3uyDjPNuBIiyW0RSYeBdxNEXkqeNFRfGKqnlr6rD1xbuRVVuYd6ia6JphqdK4Vx7UxlerS24xLQeXXd9JEfj/j1fcznD/LjL+dO8Wpv2DjH47I/R1r623xRzhnauvwnk5qVw94tTfsHGPx2R+jp3i1N+wcY/HZH6Omst8Uc4c1dfhPJzUrh7xam/YOMfjsj9HTvFqb9g4x+OyP0dNZb4o5wauvwnk5qVw94tTfsHGPx2R+jp3i1N+wcY/HZH6Omst8Uc4NXX4Tyc1K4e8Wpv2DjH47I/R07xam/YOMfjsj9HTWW+KOcGrr8J5OalcPeLU37Bxj8dkfo6d4tTfsHGPx2R+jprLfFHODV1+E8nNSuHvFqb9g4x+OyP0dO8Wpv2DjH47I/R01lvijnBq6/CeTruf8Y4an/wB7vr/h3umVUa03GMNujF2byLKZEQ5cZs24cWGpEzH49kM1MkQnDVE4d+EURN0ROarXX1b1RkaTY8eUFpvlGU2+M249OcsbluFYbYInhODMlx1JF35I3xryXdE5b52NuU3K40ZzyjL+sz9V7DUVUUzpd8t6pWjWDWDE7hLg4/lL8fDMsuDDstrFb7eLb327mBS3f6qLJeEm1EFLiEyRE+dwqionXzXXvSbA7fa7jes3s5t3mTbY8IWLjHInQnPI1HfFFcTdlV4i403RRA1TfbaqawoNKwGWZ/gmBBAcznNbDjo3SSMKCV2uTMRJUgvE011pDxmvkEd1/hXRvOr2k+OXGTZ8h1PxK1z4bD8mRFmXuMw8yyyKG84YGaEIgJCREqbCioq7ItBq4/Snc+78PzEqqtR8tQ9E2NZ4l8l5/Ai3e741Ag2l6RdYgW66x5Ul5xkYhKXE++pMmuwKqKKjsi89qDF1BwKdk9wwmFm9gkZFaWEkz7Q1cmTmxGV2VHHWELrGxXiTwiRE5p9dBn6VOLd0h9Gbm1fbnH1Fx1LDjzMV6ZkJXeJ3pTr3HWxBJSOKHEJsmJIu2xKic13ROaza56e3a3t3mVeItntjrclwZt0uEOO3szLKKvJXuPYnBVRNB4FRURSQl4aCg0rUn9XdJ4tgtGVSdT8SZsl/dRi03Jy9Rhi3B1UJUCO6p8DpbAS7Aqr4K/UtbPElxZ8VmdBktSY0lsXWXmjQwcAk3EhJOSoqKioqclRaDmpSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlBKuid9FjRv7v8e/LmKqtSronfRY0b+7/Hvy5iqrQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQSronfRY0b+7/Hvy5islrVqdetL8di3PHMWayO5S5SMhb1W4qZNoKqbghboE6QfCvCi7MKKcSKRCnjxvRO+ixo393+PflzFbhmunmMagMxGciG6gUFxXGHrZeZlsfHi24gV2I604TZbDxNkSgXCnEK7JQd/EshYy7F7TlMaK/Gau0JmaDL4qLjaOAhcJIvNFTfZUVEWtUzDONSbNmcfFsU00tt+YmwFnMTHci7j6vqnQGQDzaxyUfAcDqlBXOM1UTRgU62tsxbGLLhmPwsXx2O7Htlub6mKy5JdfVptF5AhukRcKeJEVdhRERNkRErA3zSTEMhzWFqBcJWUt3iA2LTHcWW3aHE4BJC4ShsSQjOISiKkhNqh8I8XFslBjblqPmEW+PyIeBwXsRt0oINyuTt7Vu4NvEoIRMw0YJt1oFcRCIpDZ+A5wAewcfLCzfUa65MCWrTa3ScPOa7A76/KHguDZtOk268UIo/V9QhNnsoyCcXwf3SIqqPeuOkuF3XMGs4mJfUuDbjbyx2cjuLNuedbREBx23tvpEeNNh8Nxoi3AF33Adv4mkOC/LBc3KFdHJ/Xd0jGcvc47aEjffugbcTywxe33LrUZQ+JVLi3VVoNYwTUbIbxl1w0/tEqNlJY/dZzOSXSbMSK/amiMihsC3HidS+8oKP7tSaIWkEzUlMePI4Jius9tfuK57qczdmpNoajRO54UdtY09HpKuSU2YDfdo4ooJcQ7tL4Kbqp5ez6SYTYckj5da2Lw3dI7clrrTv8APcB4H3SdNHmjeVt/YzNQ6wS6vfYOFOVbXOalPQpDMGSEeS40YsvG31gtmqLwko7pxIi7Ltum+3jTx0GF0+yCXlOE2XILgyjUubDbOSA7cKPImzm2yry4kXb+G1bDWLxbHoWJ45bMZtyqse2RW4rZEiIRoAonEu3lVd1X+KrWUoJV0TvosaN/d/j35cxVVqVdE76LGjf3f49+XMVVaBSlKCVayf1i6E/eBM/0rfqqtSrWT+sXQn7wJn+lb9VVoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFanqxidxzvTfIsPtD0Zmbd4DkVhySRC0Jl4lJRQlRP5ItbZSg873jo75S/rTLz2M3AuNsm3aFfwcl5jfIfckmNHbaFlLXGJIUhSVkVSS4vECGqK06goi/T+gmocayyoECVjch6dklmywxdkusBGkR5oPPwGVBglSKLYJ1KqnEhke4ohbj6GrikjJOO4MN1tp9RVGzdbVwBLbkqihCpJ/BFT+aUEL6Q+g2SapZNachsIRZ7LdomWKdb5eXXnH2xZkG2av8drJClInVqJRneETRU2cbVOf8sXR2udov3fJzvA6ymexsp4v3pOHGZs4QhResEiV5DBFTiMthRFVxS5VvsfIswOwXHIZF8sYQmHdocgbM8SyWx3FVRrurfc3NhDwue2+3hJtk4GVXCAlvs2UQlO+zGwcFqCygtObkvGgcbi/wCzFEU1VfKm2+6JQQFzo06pw4T1ltqYVIh3rEYuI3CTImyW3reAzJT7kiMAxiR5UF5rhbImkIx3Ux4E4/pvok5I/fMmizbmyFvuSX5yBe/lbe5EgDuQuIoJaCcGBGUOuJCeBXFcEfmNkSklxn51doFzvrD9hBuNaY7Kx0J9CdlPvOKDSLw7i2JKniXctlRV4fFXGWSZex3ygTJ2PsuWtxoplzOO6MaO0bKuLu0ru5kiog79YnI0XbltQSQNE9Znr+1qNItGBR79a5VnkQ7HGvUvvfL7jjy4xdfKWEhtqoSQcAkYPgUEDYkFHF67fRjzs4+LNypuMCtmlsyZTTLjyNbDkA3JRaFWvEjQqKIu3h7J4vCqwys/u4WyzMR7S0l6nnEGW06hC1FF1xB3VFVCQiTiIQVd0QSUvm7LvdB5I1WwnLdMTG9WuwDk0y+HltuG1xLPdZzIRrrIB8HuOHCfBt4VAAIH1ZaNDL9+KAqr6YwC1TbFgmN2S5No3Lt1ohxJAISEguNsgJJunJeaLzrP0oFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoJV0TvosaN/d/j35cxVVqVdE76LGjf3f49+XMVVaBSlKBUAtPT06J98UEtWqqyCPkIpYrkhL/gsdFq/1+KesOmTFhuS5tYIb7LLRo7KbibgYIvjcHby7eNPFy3+uvFVyKaopnvS2qIrzzfqyPSf0MJpH0zlOrJN+JbbMRNtt+f7rl/jXXl9K3QOCglKzsgEh4kLvTOVNvr3RmvzJxjJX7vDALfd5T4x1QJA9UO7gKiEi7bfxTyb8lTmlby9Ig3Fhph9t3q1bIUJxODfZF2Th2TZE2qOq5VS96ql7yd6YnRyZRCc1DLYvFtZp6/+jFcLXTQ6NLy7BqWKLvw7HaJ4899tubCV+e9utse42xqSsVxVFCBD2Qd9uSePy10XcOOQSvuQkUvGgntsi+LxpXnXVGqpfpH2tuj2qbpqEKptvytc1eX/APRr4Tpd9HgjUB1C3UfH/wB0ztk/x6mvzU+S9yaf4Y7LzCN/N4D4h2/j9X+VdOfFvcM90kSAEt+IwDkqfz8tNdUaql+ojXSj0JfTiaztCTbf/wCGzPdV8F0qtAwRVPPwFEXZeK3S0/8A8VfmTAlZIor3FJMt/GpIm3+PKvgmsk7sKNIktjxoi8PB5f5qnlrutk1VL9Nu1f0f1321CaXblyt0v3VczHSi0JkpuxnYl5P/AIdL91X5oRMGkvL3ZcpchOXgg0fV7/zJOf8AlWSkQQtzCjHcICTmiC8fF/nvuvLbx7010uaul+ko9JPRUy4QzTiXx8rdLXb+f7rlWzYZqVhWoRzgw+8rPW3dV3T/ANleaQOs4uDZXAFC34C+bvttz25V+UjmcyokYo6qSvt7Kil4XLf/ANa9h/0fGQO32LnHXSBdNpbYRbBw8JF3Vun1L82vVNdUzGe55qppiM4WrsndFj0adKvU23e5p2Tuix6NOlXqbbvc1VaVMiSrsndFj0adKvU23e5p2Tuix6NOlXqbbvc1VaUEq7J3RY9GnSr1Nt3uadk7osejTpV6m273NVWlBKuyd0WPRp0q9Tbd7mnZO6LHo06Veptu9zVVpQSrsndFj0adKvU23e5p2Tuix6NOlXqbbvc1VaUHUtNptVgtUKxWK2RLdbbdHbiQ4cRkWWIzDYoLbTbYoggAiiCgoiIiIiJXbpSgUpSglWsn9YuhP3gTP9K36qrUq1k/rF0J+8CZ/pW/VVaBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBXBPiJPgyIJPOspIaJpXGlRDBCRU3FVRURU35cq56UGJexq3PQbZbd3QjWp1l1loFREJWk2BC5c0Rdl5bcxSuBzEYT1/HJXZ845zZj1JKYcLLSCqEyKcPzC33LfclVBXfwR2ztKDTZrOIz8pu2FneJbd+usNi9ONNiqGyw0aNNOtmoKCbOB81VJd91VNlr7k6cRZAxjTJby3IYlHOckf9mcKTIJERHHBcZJvcEREDhEUHbkiLzrWR+lO5934fmJVVaDXZ+AYrdbhBu9ztESVcITgPLLcis9bIMQURVwkBN9t0LZNkRRTZNk2rYqUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFcE2bEtsR6fPktx40cFcddcLhEBTmqqq0mzYltiPT58luPGjgrjrrhcIgKc1VVWpZdrtLziWEuW05Hssc0chQnE4SkEnzX3h/wCoAvi+cXhbIMF69FrZG2ZQ3bsW483ze7nNz17rpSyoVlaXeFGEyZeeLySHFTZRVPGAf2eRL4WyDseG5lJSS3i+UPoU0kVIU1UQRnCib8JbcheRPGniJE4h8qDha68+BGuUYoksFICVCRRJRICRdxISTmJIuyoqc0VKpRNdFWnE7e/z/Xd4c86VNyqmrTz2q3StGw3MpKSmsWyl9CmkipBmqiCM4UTfhLyC8ieNPESJxD5UHea0LV2m7TpU/wCGhRXFyM4KUpUj2UpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSglXRO+ixo393+PflzFVWpV0TvosaN/d/j35cxVVoFKUoFflga3W5icQYjqtOookrgcPJeW3Ov1Pr84QefZbRt+ISOKiqipsibfXvVHGzlo5eazh4zzef2JcmwXSRjc4I0V+E4Iw5UdVQlaVeSKm3PbxKm/k3qmq4l1iEYNCb7OyGyvJd9vHt5UXmqc+dYzLdOrHkTTt0ZmdVMZEndwXZxPq5bfwSufErQ53EiXO+Ic2MAgygRl6x0eHfYiQttk3Xmqf+teaLsVxlO9NXR3viJFOA2IE+/wBSZeC2Qc2yXn5FX+W1ZmI3LbhgicXAm6o2Xi3T6/8ApXbm2ia8rbTLgoKKJoaeF4XPbbmm/wDnXK/EvdsBmPOWOCGm6iCbcv4/Uq/Xzrszmif2KEY4pPSm0aRUXYQTbnWsy91ccZjkHCi7LvzXby/47fzrISbi6GzEx4AH+ym/+WyVE3+kKruo0bB8dxCVcm35SQx7k3KQ66pbbA2KLxc/J41r3bt1XM9FyZyVcmZFtQ223TAC3JCRF5/41in7XBfLrnQ64n0RXFd8Zfwr1BjHRB1Fya0sTb7Pg4+TgoaR3xV10d/7wguyL/Deunm3QgzyDZplwsGWxLjIjMOOjFjxFB99RFVRsEM+HiJUREVSREVaRRVPc5px4oBAHuxeCHG4mGgRslEd9/4In8K/t7ZYgti660fGaoOwJxKP1b/VXjWLrTrzp1k861XkZsF6NKUZ9qnQ1bQDRdlAhVEUa9T47mLWd2aBcnmShFKji/1Zbqgrvso7+PbfxfwqS5YqtRFU7nYnPa+bvY48w1cYFsfBTiUvGqrXrb+jmhJC/aG2hKqKtq238n+9/wCfj8deUbvb2/BZJ0/Gu5NmqL//AMr1t/R4susrqALpCX/wrYkXdV/3vx1233PFe6XselKVOhKUpQKUpQKUpQKUpQKUpQKUpQSrWT+sXQn7wJn+lb9VVqVayf1i6E/eBM/0rfqqtApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlApSlBKh+lO5934fmJVVah+bZZF096RDOU3/AB3MJVpmYWlvbl2PErpem0kDOI1bNYEd7qy4VRfD23TxVm+0tp15uaq+yfKvh1BVaVKu0tp15uaq+yfKvh1O0tp15uaq+yfKvh1BVaVKu0tp15uaq+yfKvh1eTtZ/wCk+k6AdIRzFcmwW7X3Ty9W+LcoCu45cbBe7YioTTwqzcWmu7AVxkzEkFsf3ij1iqCige3smzbvNOGy2e1FdboraPONdcjLUdtVVBJ1xULh4lRdkESJdlXbZN6w3y6z3zLsHrC9+jrUcCzK1ailes5ssa4sQbxKivxm7jCciSQaW3RCQTacRCHmRL4tl33RVRUVcbedQspx7OrXZbvhUNnG71cEtEG5jeUKc5LVk3UJYSNcKMbNmnH16uIqbq0g+EmvRh7NNFOlTnMxHj3x5Szq71yqqdGcss/DuUD5dZ75l2D1he/R0+XWe+Zdg9YXv0dRTF9cs8uNptebZPppZbbht2uCW9ufb8lemzo5HJWOy4/FOEyAtq5woSg8ajxIuxJuqUO/Zn3kzTFsQ729d8pe7v8AtHXcPc/c7SOfN4V4+Lfbxpt4+deos4eYz0P+3V5m5ejZpenRtHy6z3zLsHrC9+jp8us98y7B6wvfo6j2adIVrCn5SysPkS4kDLG8clusStzajrBGY7N4ODckbBS3bRd1QFVF/s1s0zVvHbRPycb6vcltxtm3upMa45JzFliStttMNArhmqiIiAIZGpIgpvsitVh+GOc9XdZe4vTo3v5dZ75l2D1he/R0+XWe+Zdg9YXv0dTCNr9h90yrEcbsMW7zwysrg0kjvROaWA9FQVNqSBsIsc9y2UXlbIeSqnhJWyZVqdheF3SJZ8huchmTMQS/cW+TJbjNkXCLslxlsgitKW4o48QAqoqIvJdmpw/DHOermsvcU8o6Nr+XWe+Zdg9YXv0dPl1nvmXYPWF79HWkRNZdOZ2TzMQjX50rjC7oQyK3yRiumwm77TMom0YfdbT57bRkYbLxCmy7Y6w9IfSDJLbPvFryt1INutrV4ckS7XMiNvQneTciOTzQJJbJdhQmeNFJUH5yolNTh+GOc9TWXuKeUdFI+XWe+Zdg9YXv0dPl1nvmXYPWF79HUds/SPx6+Tru1DtUhqNa7ottFJUeazOkKlrSeqJCWL1zbqD4PVOICqicSKqqIF2cT6ROIX3Dxze9oVkgpZLbd3Y7seaUwClk4AMjHKMBuqRt8DSNcZuqvIE3DjarDT+zHOerusv+M8o6Kz8us98y7B6wvfo6fLrPfMuwesL36OptM6QmlMGzwr07ebq4FwKS2xEj49cX56OR1FH2zhtsFIbNvjFSA2xIR3JU4UVU5brr9pTZwtT8jIpUmPeIke4MSbfaJs1hmK+SCy/JdYaMIjZqvI31bFeE+fgFs1WH4Y5z1c1l7xnlHRRUz7Nml45OD2o20+cMW+mbqp/4RcjAKr/M0/nW4WG+2/I7Y1dbaZq04pAQODwuNOCqibZj/ZISRUVPrSpXieXJlE3I4aQO5ksF3K18fW8fX7MNO9ZtsnD/ALXbbn83ffnsm2aYcnstbTkIX1OFPIm8CIS/5kSr/NVqHE2LUW5rojLL4/VNYu1zXo1Tnm3ilKVmLpXBNmxLbEenz5LceNHBXHXXC4RAU5qqqtJs2JbYj0+fJbjxo4K4664XCICnNVVall2u0vOJgS5bTkeyxzRyFCcThKQSfNfeT/qAL4vnL4WyDBevRa2RtmUN27FuPMu12l5xLCXLacj2WOaOQoTicJSCT5r7w/8AUAXxfOLwtkHnpSqUROec7ZlQmZmc53lKUro68+BGuUYoksFICVCRRJRICRdxISTmJIuyoqc0VK2LDcykpJaxbKX0KaSKkKaqIIzhRN+EtuQvInjHxEicQ+VBwtdefAjXKMUSWCkBKhIokokBIu4kJJzEkXZUVOaKlciaqKtOjf6/r+jtFc250qVbpWjYbmUlJLWLZS+hTSRUhTVRBGcKJvwrtyF5E8Y+IkTiHyoO81oWrtN2nSp/w0aK4uRnBSlKke2lZXqZDwfLbdaMsiMW+w3aK+UW+OS0RsZjIE65GeBRTq92ANwD4lQurcRUFUHj12z9IrDustUDNWJWN3K+ONlEjHElyW4zEhxRhLPkgx3PAefREUWXnBVSJBFTKs/rNp4OqOCuYkUS2SeO5W2bwXFvjZ2jTGXy5cJeFwtkg8vGqc08dS3Ufo33/KNVJuU2tuJMs+RSbbIuSy8zvttGEsTgRUS2QHAjXHiFsVFX3GlAvH1gogIFZsmrmCZFmEzBrTcZ7t0hG82RuWiY1CecaVEebYmG0kaQbarsYNOGQKhISIoltuNSDCdNs+sOq10yWQ3arTYH3ZTpBa8iuL7V060lIFO0vh3Lb3RJVM3o7hE8e5FwoailfoFKUoFda43GFaIEi6XKSEeLFbJ550/EACm6rXZrT9V/+DSD+y5dLS2SfWJXCOhIv8FRVT/GpLNEXLlNE98xDxcq0KJq8IY8tQsslL11qweIMYubffK7FGfVPIqttsOoP8lLdPKiLyr+fLrPfMuwesL36Ovmla/Z7HB/WerO113i9Oj6+XWe+Zdg9YXv0dPl1nvmXYPWF79HXzSnZ7HBHOeprrvF6dH18us98y7B6wvfo6fLrPfMuwesL36OvmlOz2OCOc9TXXeL06Pr5dZ75l2D1he/R0+XWe+Zdg9YXv0dfNKdnscEc56muu8Xp0fXy6z3zLsHrC9+jp8us98y7B6wvfo6+aU7PY4I5z1Ndd4vTo+vl1nvmXYPWF79HT5dZ75l2D1he/R180p2exwRznqa67xenR9fLrPfMuwesL36Ony6z3zLsHrC9+jr5pTs9jgjnPU113i9OjJ2LPZEq5s2bJbINqky1UYjrMrumM+aIpK2hqAEJ8KKqIQIioi7KqptW4VJck8Fm1uDyIL7aOFfKm89gV/zElT+SrVaqhjLNFuYmiMs1vDXKq4mKu5Kuid9FjRv7v8AHvy5iqrUq6J30WNG/u/x78uYqq1TWSlKUCvzalCc0HSbeURRPmKvDxJ9e3j5/V9VfpLX5qXBIxl3WDxNOCKqIkXLdP4VRxkTOj81nDzlm6+Rx+rZCQCohMx1420Hxpty3/j461mCTrrgvvojYgibEhbeTZE/9ay825q62UaUaIsgNhe8a8X1LWs2uNNauT8YpEVGhPdONtURPrTdEqvRRknmrY3RrJVeALeDrZup/syAuSJ/NPF/jX9ekKLqbg7JdNOSCm6Kn/iVa/lujRS4BigAEabO9WRIK/Wv+O1dS63KbZ5bkyK0j0Ydh3PchTbyKmy1NlmhmYfU6NAjtnJkIAPEm3ETfNf8fHtW26Taf6MdE3Bsg6XeWQg763Fs4mPMSVV7jkHxbdSKDxAThCqKXkASXdEVd5qWbddxhIFl1xV38BPB4V8SpvzretQbu1r1oJB07fyW52dzFyQ32oaqrEthF8DrB+aSpy2RV8abpXucRThaZrrzy73mqM4c2A/0vmHxbFbIGpOBX2Xd0bXvhOtjbQMqakqp1bRFvsg7JzXmqKvLetThf0r17sGp9xkPlGy7A58lTiRnLeltuNvaVd0BFRSBxRTkvERcW2+4+KvBWra5Bgl5Wzx7eg2/ZFamE2hE+KpvzVOQr/BK0tu62+5W9tpxpW5QLzJPESfXWrY1WIo1lG6XimKaozh+9OXz9BdaLBZ8gBqwz7hchYlg24DJyDbVEVQdRN99kX+OypXlfXrQeRpvlEe74kKfJ+7ATkcN/wDYHvuTX8UTxp/Bf4V+cOD6g5rp/cRueI5LMgPInCqsuqO47+JfIqV6QhdPvWqfjCYjnTtoyW1HwqKTYTYSWVTxG261wrxeP5yLui7LVK57Pu62a6ZjLwS6WjTot37qmSXCCbHJpxteAwLku6eXf6lr2B/R6vEbuoDKsmIgNpITL+1uszl/NNv+qV5bsFwsuf49GyKwGrgvD4S+JQLygW3iVF5V656BsV+OzmqyWuqdLvchBui8OyytuaeXZf8A651FbmYr0ZeK9tOb1hSlKtIClKUClKUClKUClKUClKUClKUEq1k/rF0J+8CZ/pW/VValWsn9YuhP3gTP9K36qrQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQKUpQK6ki7WuHOiWuXcorE24dZ3JGceEXZHVjxH1YKu58KLuuyLsnNa7dRvWm8piepmmWZT7Fks+021y8NTHbHjs+8OR1diiLam1CZdcFCJFTiUdt/LQWSurbrpbbvHWXabjGmsC64yrsd4XARxs1BwNxVU4hMSFU8aKiovNK8w6g3/OMr1txS74ZbNQ7Zb40qzSlJYGSNRbhbJBoMjrWOJiBENriVHG5TL8lULi4WRbRxJdLw/WTHsetWOY3ddRcYtke65G7HKNYMhvEly7ndnTjqXc0+KqMmwTZA5MJyASqaubLxKoe+CIRFSJURETdVXxIlde3XK3XiBHutonxp0KW2L0eTGdF1p0FTdCAxVUJFTxKi7V5oZxTVCblj14us/P3HZeX3CzyGUuE5mCtmctBEhDGBxWGw7qQeB8dyE14Qc2XZdC03sOpWKXDRWFbYupZMxbVa7dLskoL/AA2IXBxpLfckbuwHBHwxKPcGW3CFAKM+OzYkHt2lKUClKUClKUClKUClKUClKUCpqPRz0cc1cuGulzwyNds1nJHBu53IilLABloW2xiNuKoRuQqSk2KEpGaqq77VSq1+2ag4NestvGBWrLbVKyTH0aK6WhuUCzIgutg42Ztb8aAQOAqFtwrvtvuipQac1/xhmf8Azdj8th1N8twDOMs1Fs12fs+IRYNjnty4eTxpDwXxqKhCTtvFlWFFG3VHhccSSgkK82d0RapDX/GGZ/8AN2Py2HXereiM6KfhHpDJmcqqvjPqgeL6W6xnidm0qyu34hbsZtd0Ce/drbfpUudMaZmrKbaSKcJkGeMkBCLrnOFEJEEt0VKBqViuW3G641meCsWaZesYkSCCBd5bsSNLZkNdU4KyGmniaIfBNF6lxF4eHYeLiHfKUiiIjI0pzzR+1aS5dInW3I8nfsqXGTlzuS3eFGecejMNFbjhjGYcNoSf2Tq1UzBvi3NeEeQrqU7oy5P3my6xxshiyoLlztE/FY6XCXbXWY0HiIIT8qLs8yiKRNg+zuQggKokoqh+jawWdZjatPcOvGcXxqW7b7HDcmyQiNda8TYJuSAG6cRbeJPLXJopy2uxXV3JZgOjmY4hdcYvvcFladYulzlXmOeTXS5uNtSmG2wMJk4XHpjo9Q2i8SRwVC5InD4e0X7FdR7bqNMzDARxuQxkFvhW64FeJUhsrekZx4keZaabJJPEL5IrauMbKCLxrvsmPjdIa2nekstx02zW2E1dItrmyJTEJWYRS+FIjrhNyTUgeUxREaQzDf8Aeg0nOuzF6QWIOXiVDuVlv9qtDZT24V/mx2kgXJyEJFKBhAdJ9FBG3dutabQ0aNW1NERV8xoxGUO+81uFo1qB31iWOfKx5MYsN3u99ts1qS+dwlvzRkILDzJNcDINrLd3cF1xTQQ8AOdf2foPksvHrLbGLlamJFkxCBaGVQ3eqK4RJUeQ3uiCi9QRR+El5FsXza5YHSNfLIrwF+wS+2a1RbNa5tthzIjSXG4SZst9hkG1B82dnFBrhEyAgU163q08Wft2sTt+vWM2uBYJdoen3qZZr1bru0HdkB1mGchB4mHTZVVRGiQgNwFA+S7+JEUSZ1Netmk+pV0y1czy5zGYT8m/99nIdulyJAR2e8xQerR1xltXj60uPiUG04V8W6eFiLZoTqTCxxhSk403e7RbMbZgMpOkORZEm0yHj4XXOpE223QME4kA1AlJeE+BOL0NSvWrhzTlG8O0jzGHmreoWUO2Nq4XB+7TbhDgvuPMxXZLMRhltl02gJ5BbieG4Qtqqmuw7ck0XJOjlqzeMatOIjkEJ6BBxeHam0DLLrbo8OazxK6Sw4oI1cG3v3Y7yFTq0Hk2aKoL6epSbcTsNOWqYLilyxmdlUq4PRjG+XsrlHRkiJQaWMw1se4psXE0Xi3TZU5+RN20x/3nL/8Anwfl0KupWq2bRDRfUq/ZVfdRtIcKyq5MXZqI1MvdgiTn22BgRSFoXHmyJAQjMkFF23Ml8q1HiYys1frvh7sTndj9dzetY9SW9HtMr/qfJxm6X+JjcdJ02Da0bWSsQTHr3QRwhFeqaVx1UUk3RtU33rW9G+lNoLrzjj2S6a6jWye3DYWRPhvOdzzYIJ41eYPYxFF3Tj2UFVF4SKtO1j6EuiGcaZX/AA/TrR7SnEL/AHmOMKPffkNBdO3tmYi860IABdajKudWSGKiagW6bVjuj1/R69HXo725xyx2KTfMjlRijScgurvHKQS+cjAjsEdN/EoJx7bIRltWLVnlOjvaM55bHV1+1ov1tivXSBZ3pUSJZLpebXZziiqzXYgtEL8gjfaQBFXEJGtlVURV3E0Ea/pa8WuJOhQLhhuSCHFAjXme03Fci2SXLEFYYk8MhXCIlcbRVYB4A6wVMhTdU7eqWktwvl2bsuQZAUZt2yXazQJ3cqGkoZjbY7l4QoLzaNbqHicRdxUdlRNNufRegXjN4Ob3J7CJc4zt8i6y5mDxpk8n4oAKdwSZDrncTZo2O4ED6j4SgYEvEmdRlOc1fi7/AI/rczs4mff397bImtiXa1zrpj2lubXgYt2es8ZuIxCQpz7LrjbxATkkQabBWiVTkEyhbigcREg1qt66Rs8X4Ey0YddI1ml4tdr3KlS4sZ123yobzbRMuNDMBTUCUwJG1USJQ4XOHiJMjlfR8TI8MtuJnerJNbt+Qzb4ca/WDvlbJiSXXz6mRD69tHOr7o3AlPkbYlw/2a6AdGd2FilvxC0ZjDiQ4dlvViNEsiIKx5zwuh1QNvADStKAJsiKJCioghy29xokaLbJGt9gh5YuNP49f1hNTY9pkZAkdlLazcXgE24hbu9fxkhtpxi0TSE4Iq4hbonxp/rnZtQb3EtEXDsntDV0t7tytU65MRhj3Blkwbe6vqn3DAgJwUVHQDi8YcQ866ErRG7SMpffDN2m8Tm3mLkcuzpav+1HcWBbQeGWjqILCky0ZNqyRqQl+9QS4UzGI6UfJaRhz/f7ur5J2WXZ9u5eDunrzYLrPnrwbdR83wt+Lxptz57uTnu5Mrc9SsfsF+7w5LFutn6w0GPcJUBxba8i8KCqzAQmGVUy6sQfNtwiTYRVFFS6uoOqDGBTrZaGcOyHJbneGpL0SFZgjdYQsICuqpSX2Wx2E+JNyTfhVE3JREsFdtBIlz1NTVNrUbKItzbdE47HcloltxG+AQcYjuyoLsmO04g+GDTwIqkRJsq71t95w/vtmFmyvvj1XeiFPh9z9Txdb3T1XhcXEnDw9V4tl34vGm3PmxzYnOb6h5fd8Si6hYm3ZGMOW2Q7m0xdoby3G8Pvlu3Fik2+Cw3x8ARIm3lJ1wUEU4dy9AYRmss3WMayo1Gc4G8KWaIKTBRN1AtuQvCnjFORbKQ+VBltl08gYrjOF225XOJKZwZlF7qkMOBxkMY2etFBdQWy2Nfno4myqibKqEm+2fT6JnsVyVntkYlWGS0bbNknxxcblNmKiRymjRUISFVRGiRU2Xck3VEHkTMXI1e/6ef08/mktzVpxofqFRpXnzVDQLoz4PjLU+z9FnSSZdLjcYdot7cjD4AsDIlPC0BuqLCqjY8XEqJsq8PCioqotYBzo/6NaZ49cLlqz0a9E8nMXYzFqcxTTqFAeuMp9zqwhpElOPCBqatojpSkbXjVS6pAUl0mg9RUrx9dI/QytcdEPoS2CRcY0O4T7ta2cGsHdNpaguNBL69ScRo1bR5s9mHHeMV3b415VzZRinRWYyaFjmHdELTydFG+2yz3G+u4NaVt7LkoBdKMKIov9ejJtnxK0rScaCpqe4oHruleIMQtPR0yHKo9mmdE3S2NaJEHGJMe6lp3aUSW5cyfQx6lJBEwmzSIKr1nAoub9YihvsbkToTx4Dl0mdDnF48WQEeRZXDwSyL39ivS2ooyIqCSqII7IZ3GQjLnC4hICpQevKV5Iv1l6HmN6bztSLv0KsQiM2e4PWy8W6ZjWKw37Y80qIXXPyJLcNUXjb4erkGpcacKKu+2x6XaNdFvUifkb0boyaSjaoLlvO2GmEQAeNiTAYk7uorapxbvKnJERERE5+NQsOsepLej2mV/1Pk4zdL/ABMbjpOmwbWjayViCY9e6COEIr1TSuOqikm6Nqm+9TKy9JnRHpF6bpdtJs9gXd1u6WZyVbiLqZ8RO+UZF62Oexim/LjRFBVReEl8ddfWPoS6IZxplf8AD9OtHtKcQv8AeY4wo99+Q0F07e2ZiLzrQgAF1qMq51ZIYqJqBbptWl6cdA3QTos4i1e8QtEm8ZYtxtLL2RXZxHJPCVxj8YsgmzbAruqeCPGorsRlU+F/Po+MeqK/+VV8J9F5pSlbLMYPOrhkVpw29XXEokWVeIUF6RCjyRImnnQFSQCQVRfC225L41StRLVwZt0x9+zhFWxSMaeyq8ynhVTjQ1Ae5xHYk4SMlcXmi+CyabIvNKUqIqbKm6LUTsnRpbtGH5Lh7+bvz2ckubKuPPwA42bG0/xhaB4STdtGyda41XfZ0lUV8virSz2PVOXe4Lxrbm9k0ay/Lr7BsVlyfHEYkIEtpw4LMWSrZsOuj1okSA24QGvWAiuMObcKck2PRrPn84euZ/t00v1CZiC2nDhkFWCiESrzeLvjLRUJE8FNg8S818mDybowYz3Hd7dpQ3jmn8K92tIU2FbccaGM9JbkNvR5RtsOMoRBwuAqeMhc24h4edBw226qQZMk9QszxS9RybRI7dmxmTazbPfmpk7PkoabeRBHbx7r4q5EVZ7XZmnLYnMnP9a3ZGe5VaLjhj2P4RdZMZLG7ZJIz5seOw265tP7t6ptwkMuFVjKO6Ii+NSTZZ2veNQ7xGgM47kE23K5BYuN5issFCtT8wQKM1IQnUfUj61rdWmnBDrBU1BN1TDztFdQpUzL7RG1NssXEM1uL0y4wgxl1bo208022601NWb1QqQt7IaxiVEJdk32VOlkfRZxi8ajMZxBiYaDRPwH5S3TDYt0ujSxBAGwhTnyVIoELYISK06qLxE2rZLxJz343O+7O9t5612Fh+8SpWN5E1j1mamG5kiRW3be65EUkkNAjbhPiQkBiiuNABkKoBGqpvjmukFZkt01bjgmXW6/xnobDGNyGIi3Gasvi7mJpW5BR+E+BzmbwcHVn1nBtXB+xC9PWy/4PJz5scGvKXIwtcazAE1t2aZuOdZLNwwcAHHTJsQYbJNhQzNEVC6h6F5bcEk5Jf8AUeBLzVt63OWu6M2AmYMMYSuK0Jw1kkTvH17/AFqo+Cl1ngdXwpXc6z3XzYukFPfs77tx0/vlyv7t7u0GDj1qZjtz+5YRohuOd0yQYRQQgQlR3YlMerQt0rZ8X1nsGaZNCxzF7DfJzUuzxb4dy6llmLGjSetRpHEddF7jUmTFRFslFdt9k3VNDybost5RaIx3y8YnkF+j3S43NH8lw5u6WxVmqCuikEnwUVAgHqzR7iFE2Lj3LegadaUxNO5xyYNyZdYWx22yhHZtrEJsO5FfVXBbYQWgQ1fXwAbER4eW+/LlOnntJ0ctjfKUpUqNiMl/3a2/8+s35jHqtVGNQbHZcmx9jH8js8K62u4Xi0R5cGdHB+PIaK4R0IHGzRRMVTkqKiotZLsndFj0adKvU23e5qhj91Pz+i5hP2vkdE76LGjf3f49+XMVVa8v9GToydG2/wDRt0ovt96PmmtxuVxwexS5kyXicB5+S+5AZJx1xwmlIzIlUlJVVVVVVapfZO6LHo06Veptu9zWcuKrSpV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaDUdFenHoZrFkUrTyRd3cN1Atsx22zsUyNQjTAltmrbjTRoStPqhiSIIEp7DuoDXg8byisbszDcRP7SDxIi/Xuvjr0/ob/AEW+iWA5S9qZqnHg5plcqedzCHGgDbrBbXicVxAjQG1VFAFJREXCINhHZsVSvE4ZBd7KwEb90/EJUQCaVOHn4t033Tx1BejPJNa728uSpMuAIOuqSC4iqZfOROfP+f8AlX0xc2gRwXZDiltvuYom/LxbVqMe/wA0lJyOTfEo7IAlvt/Fa7trcF8XnZ3huEPEJEu23hIi/wAPLVfLJPk3W15H3BAQyMkVV4fGni8tUrSzS3LtaGnZNojpbbQyatP3GaK9WReUQFOZl4uScufjqVYXjMzL8qt+KWfZ+dcnxYDnugiq81+pERN1X+CLW59L/XDpJ9FHUnFNOdK7PHf0872RSb4bd1hS3kNUko68PMDJefLbkSeOvMxVMe68VRt2b1OsHRPtULISsM2aF4MOJWnldUBVPKitDzQUXlzPfddtk8dda+aBagYzNukCX3W9anWm22Vi29AjCJL80EaMl3Q0aVTVEXYS5LutehdNHWBillT7ZDIubTTnBvv1QcO4tp9SJuq/xVVXy1PdQP6QTSTTjU+LpJcrbeJd6elsRHlZYQWmCd4eBSIlTdNiRfBRaxbNF32jNVNdU/CIzyRV4e5G2qdrxxq1j9hgPyLQzKiSoTa9yNnOEJAyOrHg57j4K8vKqbIvlWvKeT4jgVqvhhndluFjWRJPaTa0AW90RFUUHYgVOaF4gXZU8lfuvqDhukeX2hwdQMQtk0CHr1UoqHIEtvnCYJxoW3lTnX5NdM3G9GXrdNl6T59NmQTnMxe4rzAcZegzUQ16tuQaCpt8KEhCabt8QqpKhbJqYLBXcLMRTXnE78tkx8P7orVNducpnOGq4d0ctG8sIJEHUW6OsOohgIttIaIv1ptuv+Xkrdneg/i1xt6uY5qarksd0QZUdARV35Iuy7pXjTHtQMixkxjJLkpGRV8FtzgIF+sV8W2/jTxL/Dx1VMG6QmpEi5s2q2yu6esJE6wkUVQf4pvslXLlrHWpmabuceeTToqtVbKo2vS2hWjuo+k8/IrbeXrfKtIA2+LjUjiET32UttkVOXjT+CV756IEYmouTvkqkr3cK8W2yFt1/iTdeXOvLWkV/hLZkau8Ga+7cgVuQRtbgakmy78/F9VeruilKGS5lYiv+z7gFE22RB/foiInkTktVcLiK8Tembm/+yPE2otU5Q9A0pStVQKUpQKUpQKUpQKUpQKUpQKUpQSrWT+sXQn7wJn+lb9VVqVayf1i6E/eBM/0rfqqtApSlApSlApSlApSlApSlApSlApSlApSlArhlzIkCM5MnSmY0dkeJx140AAT61JeSJXNWu5BhzN5uUS+x5iMXCCqFHWRHCUwiou6L1Z8wX/xNE2S8tyXZEQOqOqGFuZGGLMXmO9ON8YyI2+2Q9YokW3zt+XBwry+cQp499s9Lvtkt86PbJ95gxpktdo8d6QAOvc9vAFV3Lny5JWnWnT7IY+auZRcsodcZWRIkC02LKqvF1YAK7s7onVBsWxbpy2XmSr2Lvh18uN2ubQ97itt4kRX3pRumkpkGeFUZAOBRVFIN0LjTh6wl4VXxhk7Zn+PXWPd7gw+g26yuE0/OJ9lWTIfncPCakiJ9ZIO+6bbpXzZtRMUu1sbujt2h24XWlkIzMmx0cFrl4ZIDhIKbEPjXdN03RF5Vwji14DT5/GBlx++Upl0Hn+MurU3jInFRduL+2W3L6vFWNuGCXc4s4oaQXXp13GY82sk46uxWwQWW0fFsjaIeES3BN0XiRCTfeg2d3LsTZhM3J7J7S3EkoqsyCmtI24iKgrwkpbLsqonLyqlfS5ZiqRJM9cltSRobvUyHlmN9Wy54uAy32Ev4LzrRIul18W3SGZ8mC5KOJIjMGUh57qikyCJ8+NwVNV6ngFCVVVVRUXku689100uTt0W7W8Iio3LRWYbdxkQASOEZGmtnWB4gMF4/BRFFUNU3Sg3cslxwHYTJ3+2i5ckQoQLKbQpKL4lbTfw0XdPm7+OucLtanLet2bucQoKCpLJF4VaREXZV499tkVFTx+StHYwC9w51uZiN2sILAshJcSS+SvNi4rrjZsPC6Lu7hEQudYJjxeNdtlyDuDTY53G8Q5UOTermCtOKbAxYiCXgqRNtipvKg+JHjPmnJQRVoMxdsst1tdbhxWnbpPddVkYUEmye4kDjXi4zEQ2DYvCJOSptvum/RHUK1yShjarZdLl3TGblu9yMCaxGTLhEnUUkXffi8EEItgJdtk3rBy9Pr43jcGwQQtshyC5JRua9Nkx5CIe6C+rjKIRHsSobfzT5eEicq6iaSO22cx3ojWx1tgYKtT5DzgS46xhTcGkQCEUcUU4jQkVEMtxPZEoN4uOWWC2wDuB3JmQKNK621HcFxx5ONARGxRfC3NRH6t1RFVKxxZ/CFkW+8t0W5lLWElqEWVk9agI4vNHOq4UBUJS49tlRN9+VYiPg1/Ztt5bltWefIvLrUt1tx15psD6xSNlswHjbAd+IHETi41IlHdaxJ6RSwhsvnCtN1nOnNdks3GU+4w24+gIJiZCZvK2jYonGiKS+FuK7UHI/pnneQyHb9C6SeqVjj3Fw5TVrC2Y2gwQNVJGBR60uO7Ai8KcZmWwpuRLuS/H7G9RfSx1V/DcV+DVSrTBK12qHbTlOSSisNsq8585xRFE4l8fNdt67dBKv2N6i+ljqr+G4r8Gp+xvUX0sdVfw3Ffg1VWlB510RxDVrUnRrBdQ770qdSmLlk+OW68S2olrxcWAekRgdMW0K0ESAhGqIikq7bbqvjrdf2N6i+ljqr+G4r8Gp0UPovaRf+R7H/7FqqrQSr9jeovpY6q/huK/Bqfsb1F9LHVX8NxX4NVVpQSr9jeovpY6q/huK/Bq8S68dA7pJa49LNrLMb1QyKx2jF7fChpn97O3sXCS9wk6SwWbUxFU+rR8W+J1AXiBxEdVBEU/S+lBCtNsWvWFJkGM5Fnl4zG4QrjHB68XZuOEmSve6HzJGGwHZPEiqilsnhEZbku5Vkcnw26P3ZzIsWkRAlyWwbmRZikLMjg3QDQxRSbNEXh34SRU2RU5ItYjvFqb9g4x+OyP0dbVu9bqop2xuiNvlDMuWq4qnZ3ualcPeLU37Bxj8dkfo6d4tTfsHGPx2R+jr3rLfFHOHjV1+E8nNWv5/iny5wu8Yh3f3F32iHF7o6rrOq4v7XDuPF/LdKzXeLU37Bxj8dkfo6d4tTfsHGPx2R+jprLc/tRzh2KK47p5NBvekffi5Xi4/KDqe+t4sl24O5OLqu95NF1e/GnF1nVfO5cPF4i256410dnpclyy5LmYXHDIrt1ftVnZtfc8lhy4A6DyPykdIXwAZD6NiLLSihJxq4o7rYe8Wpv2DjH47I/R07xam/YOMfjsj9HXnStT+1HN3K54TyQ+7dGm65jbbpF1Hzq1ZE5Kttst8RpzGGxgt9wSjkMHIjOPOJI41NBdFSASQfARrflm9P8AQKLg44+4xLxuG5Z7vLu70bHcWYs0Bwnopx0baYaMiBBQkJScceMlRU4kThQar3i1N+wcY/HZH6OneLU37Bxj8dkfo65nazz0o5/3dyuTsynk5qVw94tTfsHGPx2R+jp3i1N+wcY/HZH6Ovest8Uc4eNXX4Tyc1K4e8Wpv2DjH47I/R07xam/YOMfjsj9HTWW+KOcGrr8J5Oau3ph/vGXr5Fvwfl0OscmPamPL1a23GIiFy67vpIkcH8er7mb4v5cY/zrcsXxyPjFqS3tSDkvOOHIlSXERCfeNdyNUTkieJERPEKInkqvir1GrmmJzmU+Ht1aelMZRDL0pSspfdG9WW25DbXrVdY6PR3kTdN1QhJF3QhVOYki7KipzRUqYyY1yxi5BYb66rwvKqQJ6oiDKFE34D25C8ieNPESJxD5UGuV0b1ZbbkNtetN2jo9HeRN03VCEkXcSEk5iSLsqKnNFSq96xrPep/F6+U/rYgvWYubY3p1SutJjXLGLkFhvzqvC8qpAnqiIMoUTfgPbkLyJ408RInEPlQezVOJz+KjticpKUpXQrjkyY8OO5LlvA0y0KmZmuwiKeNVWkmTHhx3Jct4GmWhUzM12ERTxqq1kMSxORkMhjI8jim1AaJHbfb3R2IyTmL7wr5fKAL83kS+Fsg821To073aaZrnRpMTxORkMhjI8jim1AaJHbfb3R2IyTmL7wr5fKAL835y+Fsg0elKv2rUWoyjf3y0LduLcZQweaYfaM7x2Rjd5KS0y8bbzT8V1Wn4z7Ro4080afNMHBEkVUVNx5oqboukOaAWy42u4N5PqFl9/wAgm9zdTk0w4LVwgdzO9dG7nbjxWog9W6qnsUckNeTqOCiClTpUqRJovRvw9oJTs3IsjuFwuVnutnuNxkvx+vm98DaKRJcQGRbF39y2go2ANiibI34tvt/o7Y05kwX+PluTxIfd8G7vWZh6KkGRcIrQtBKPdhXuMmgACEXUbXgQkBD3JfPOoWr2UacYO/bsLzOXjt7dv2Y3eJxybdHiXHuW5uqcVe6Ykp+Q+onxBHjNtkQg4pPtIiLXUyLUrP8ABpGfOY7qjAsw3vUNvvper3doVrj2SOdljOsIkpy2zGmBeMUaEpDBoaAgiYmXEoei7R0aMJsk+2TYl9yEgtcSzRAjuPR1bd72G6Ucz/c8XEvXOCfCQiqKmyIqb1wNdF7DOoSHOyrKZ0OEsVuzRpEiNwWaKxMZlpFjqDAkTZOR2UIn1dd4QQUMalcTVXWye4F3n6htxSsVsw+ZIg223NFAuh3Cc7HkqZy4jcpGzbEDDgRhULZU3FeFdJv2pOa6W2q/vWfWx6J1eoORrOhOvWdm5yneuaJiLGGXFSPKNRLj7j66LIdAuJp7weFQ9L5N0dsZyKc1dGcqyO0TW73NvSyIJQyMllsCxJjor8dzq2zbBE42+F4efC6O61sunGl1h0wiSolinXGSMtuE24Uxxsi2ixGoze3AApuoMipcvnKqpsmyJtUF85MKPJcbNs3WgMgMOAhVURVRR3XhX+G67fXXPQK0/Vf/AIORfqu9nVf5d8Y9bhXQvtlg5DaJVluImseW2oEoFwmK+NCFfISKiKi+RUSpbNcW7lNc7omJeLlM10TTHfDS6VxFjGpENe52WseujYchkvTnobhp9ZNiw6KL9exbb+JE8VfzvFqb9g4x+OyP0dbGttzuqjmzdXX4S5qVw94tTfsHGPx2R+jp3i1N+wcY/HZH6Omst8Uc4c1dfhPJzUrh7xam/YOMfjsj9HTvFqb9g4x+OyP0dNZb4o5wauvwnk5qVw94tTfsHGPx2R+jp3i1N+wcY/HZH6Omst8Uc4NXX4Tyc1K4e8Wpv2DjH47I/R07xam/YOMfjsj9HTWW+KOcGrr8J5OalcPeLU37Bxj8dkfo6d4tTfsHGPx2R+jprLfFHODV1+E8nNSuHvFqb9g4x+OyP0dO8Wpv2DjH47I/R01lvijnBq6/CeTHZL/u9sTyrfrP+Yx6rVaJZcJv0u6Rbpl7tvaat7qSI0GC4bwk8iKguOOmIKSDuqoKAnhbKqrslb3WfjbtNcxTTOeS5hqKqYmau9Kuid9FjRv7v8e/LmKqtSronfRY0b+7/Hvy5iqrVJaKUpQK/DqFBkxG0gq0htkqiIqH/ov+FfuLX4kzJzbbqosh0DAtvEuyL5dtk2/hUV2d0JrPe6cWDc0dUWQ4FRdk5f5VttltN1WOL0g3WgQfn9SpIhcXl8m1dSJFclCEjrlFUDi3Xdd03/h5f41szuSvWbHzjJIKK0bRKalvsQfV5fKm9V5qz2QsbkJ1h1MvuE3mAxh17k225QT69ZcRzq3BPybKP+PKvT/Rs6ZVj1pxhzE9dwgTMqtytx4MwgJClMqqKTrm6cCGioKeCvPmuyeX8/M5vZ3a/TZrp8auulweXlv/AJVgoF0WyTQuEJwRlNLxAfCi8K/XzTb/AKVo3cBRfw8W6t/i8UVzTVpP2syHWHGoFoWPByyDBQA2EyeFETl//FeC28TxW5dINvVXJdShu8WNchuBsvGLjzrra7gHEnLq04R22Txcv415aZmTMokG/dL/ACXUFfCJ54i4i/xWu6wrdsdE4F7ECFeQoW/lROaf41Uw3sicLEzRc2zCau/FydsP0u1D6RuSahXJsrODKQnAQVfZmqw4A+JR2FOf1818taTkmmFs1J7hjTIDLgQ0LfrD6xAcPmRJz357Jz/glaH0YAHUPF3p04oDD8B9YxGTnV9Zsm+6Iv8A/avSFrwCwQYvdL8mObyIiogu8aFt4v4f5rWHir12xXNuJymO9ctW7OjFczCJSuh/ibjPCdoYccXkg8Ioip9ac96/sHocYxZrg1cbJEeiSw2VVUCQNvLzXktXRh10VFiOywyaEnVE2W6qvi4U3Ln/AJeWsTd9SZ+N3dm33THn4hcW7qSJZN8ac0RURRTwd/q//ms7/wAtiI2TXsV72MwtmNKqNjJaf4/cLPw2y5w4zrDfgr/ZJdl5KK7qm6V6E6MMFu23rNozQGgmsB9CJd90NZOyJ/LbavNialWGVLauUO+OREf4+7Y4iLvc5j4iHiFUIT28i8lr1D0ZMhh5FEvkmKw62QjD4utDhMkXrtlVE5J4lXZPrqx7JxE1YqKfHP0V72OsYqiaaN8O/aelZpLf7VCvtit+pVxttxjty4cyJpblDzElhwUJt1twbeomBCqEhIqoqKipXb7S2nXm5qr7J8q+HU6J30WNG/u/x78uYqq19YpJV2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8Oqq0oJV2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8Oqq0oJV2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8Oqq0oJV2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8Oqq0oMTieU2LOcVs2a4tO7tsuQW+PdLdJ6o2+vivti405wGgmPEBiuxIhJvsqIvKstUq6J30WNG/u/x78uYqq0ClKUEq1k/rF0J+8CZ/pW/VValWsn9YuhP3gTP9K36qrQKwl/zLHMZdajXe4EMh4VNuMxHdkPkKLtxI00JHw78t9tqzdSaxksuVe7vIXjlSrzPZcNfH1ceS4w0P8AJAaTl4t1JfKtWsLYpvTM1boQX7s2ojR3y2n9q+H/AN2/+rdy9xT9q+H/AN2/+rdy9xWMrrXS5QrNbZd4uT3UxILDkmQ5wqXA2AqRFsKKq7IirsiKtXex2PPnHRV7Td8uU9Wc/avh/wDdv/q3cvcU/avh/wDdv/q3cvcVLsP1y09zm5Q7TZHchjyLkysiCt3xa6WlqYCChL1DsyO0Dy8K8XCBKvDuW2yKtb9XIwmHndnzjo7OJvRvy5T1ZP8Aavh/92/+rdy9xT9q+H/3b/6t3L3FYysXj2TWTKosibYZvdTMSbIt7xdWYcMhhwm3Q2JEVeExJN05LtuiqnOu9jsefOOjnabvlynq2f8Aavh/92/+rdy9xT9q+H/3b/6t3L3FYylOxWPPnHQ7Vd8uU9WT/avh/wDdv/q3cvcU/avh/wDdv/q3cvcVjK68uexCcjNvBIJZTyMN9VGcdRC4VLc1AVRsdhXwi2HfZN91RFdjsefOOh2m75cp6s3+1fD/AO7f/Vu5e4p+1fD/AO7f/Vu5e4rGUp2Kx5846Harvlynqyf7V8P/ALt/9W7l7ivtjVPCnngZdnzofWEgC5PtUuG1xKuyJ1jzQgir/FaxNfLrTT7RsPti424KgYGiKJCqbKiovjSnYrHnzjodqu+XL+6i0rUtLH3nsKitPOm53HKnQGyNVUuqjy3mW0VV8aoDYpvW21l3KNXXNHhOS/RVp0xV4lSrpY/RY1k+7/Ify5+qrUq6WP0WNZPu/wAh/Ln68PSq0pSgUpSgUpSgUpSgUpSgUpSgUpSglXRQ+i9pF/5Hsf8A7FqqrUq6KH0XtIv/ACPY/wD2LVVWgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSg6N6sttyG2vWm7R0ejvIm6bqhCSLuJCScxJF2VFTmipUxkxrljFyCw351XheVUgT1REGUKJvwHtyF5E8aeIkTiHyoNcro3qy23Iba9abtHR6O8ibpuqEJIu4kJJzEkXZUVOaKlV79jWe9T+L18p/WxBesxc2xvTquOTJjw47kqU8DTLQqZma7CIp41Va6ty7rwyUtsyeUKx1Eih3M9hCQApuon5BdFE3VPESJxD5UHNYnicjIZDOR5HFNqA0SO2+3ujsRknMX3hXy+UAXxfOXwtkGlEzXOhTHvenx/W3uUqaaqqtCI2mJ4lIyGQxkeRxTagMkjtvt7o7EZJzF94V8vlAF8Xzl8LZBo9K6N6tEa+2x+1THHgafHhJWj4S/wD9En1iSKKpyJFRVStC1ai1GUb++WhbtxbjKGKv+oWH41wDdb/BBwpKRSbSS3xgfJS4kUk2QUJFX6kVPrTfu2/KLJcMdYypLgwxbX2Uf6990BBsV/vFvwoqLyXnyXlWmZFppkk60wbJZ8ljxolvSSrAtw2Y3M21AOMQbIFXw3d1AW05psO/hJsF2xi5paLNDtclq4O2aU2+jdwJGQkiAkgoRNN7Co8QkKo2vME5eWpUjmlZ9jrVwtFsgTGrm/e1JYqQ5LBIrYrsTm5GKKKKip4PEq8JbIuy18Pah4xGyCXj8i4MMlAaFyXJdlMNsskSKqAXE4h8WwqvIVRE8apXVxXD7pZ727erlJikr7Lxk0wRcDch97rHUFFRPBRBbFC8aqiqqJvtXx8i7jIyMLvcHojjA3R64oCkRKipHFmPyVNtx2Il58l223oM+1lOMSH5MVjI7W49DbV6Q2ExtSZbTxkaIu4om6c15c6+I2X4nMkNRImUWl999wmWmm5rRGbg8yAUQt1JN03ROab1Pw0vyqURhcJMEAdjOxpBBcpDgvK8+2Tzgsq2LTHE2LicADtuSbqvjTLy9NpUs3kbdhQxkXIpSuR0VDaaajq1EEfBT5i8JcO6Ii77LQbY3lOMPBNcZyO1mFt/30hmNqkbmqfvF38Dmi+PbxLXNBvlluitjbLxBlq8z3Q2jEgHONri4eNOFV3HiRU38W/Kp9B01vMS1K0tvtiyG2GIYNlfLi4itASGRNvFv3OvGDZCINltwbKqovLMBp/MutsgQ8muMZVjOHKe7mhsK8cgjVePrybTZeFURTbbbcUk4kIfFQbTIvlliCZSbtEb6twmjQnh3RxA41DbffiQPC28e3OsC3qRZ+5JEuXbbnEVthmTHZdaBXZjbpKDStCBku5EmyCXCSbpuiJXVYxC+jf5t+fW1uLKiOwW47puPA02gojRkqohOGeyo5uqLw8KIq8O5YBvSOU5HdmzIVudld0RFatrtylPxe52EJEaV5wVNEVTJeHgUU4RHhVN1UN9hZRb34rr9yByzusIRvR7ibbbjYCXD1i7EoqCrtsYkorv49+VdMc7tK3QoBxpbcZHnYw3ExBIxvNApuNovFx+Cgl4Sgg7iSIS1rsDT3II7tpJxy1BGs75SmYjRn1ZK67xm1zDwWmk4erREXcwElQdkROKfpbOvd1ny7h3DCaJqckfuSVIMXXpAqHWkya8DKoCrxI2qqZKqqqckoNxxzJ28kEnWbNdIbCtg9HelsiASWi34TBRJdvFvwlwlsqLtzrNVqeCYlIxopz70G3W4ZaMgMK3Om4yPVjsrpGYgpOGq+EqjvsI7qS862ygUpSgUpSgUpSgUpSgUpSgUpSgUpSglXRO+ixo393+PflzFVWpV0TvosaN/d/j35cxVVoFKUoFfhmkeTFaV+OSo1z3Ew3UV8v/ANb1+5lfkJExx2zk8MkWgUU8Eic3b2TyoiePx1Bfq0YiU1mY2tGtOQAywkcWWusXdDJU+d9Sc/8AGqtpNpxD1bzSBhc4lKNNBzugwNVJsEbVfr+vb/OtWvd0s+Pxu7rs+ANIu+3IUVfqRPH/AJVrNn6VuUaezp0/Tqx22NIktdQEyUyTrjQKvPhTdE3VdvHvVaYqqjOiFqMmTv3RKiaUZnIt2Q29Lm6w+rkF8206sgReSbL4Kr/OuV/Q64XiQqxdLMdfjFzRZdrjxQTdd9+MRAtv477VN770o9bsxkORsmzeScR4TQ+52Ua4eS7oJAiEP80VKmzF1yKZLSN3wdleH+7J2Osp8915cKKikS//AFyqhew2NvTNVdyIn5z0ecRcyoiLcZfF6XtPRR01cBxcxj4lZ3VXcAtWSqR7/UrRAqIv8iWuBno3dHazvq/OZudz4C4usN6T1Xj8W4tCn/Va1vG+j30l8rhpJt9muGIWifw8T1wfSE7ITyEY7oa7+NBRET/1qnj0XG9PbA1Ly27yr7JkKpOPJNJG1X+7wq0fJP4Eu/8A0qjGBxtv39fVOfdGf1mYZtVvEVZzFTqNwNGsLjjHxK4x7RHVxTNlHJSt7r499y8a/wAq2PF5c02Fk2+a5OgyjQYwtPyEFUQvCUV3XbdE8RbKnjritFgy6zW5Dxuzuv21tV4e4QkbIvl8IjEd/wCVavk2rOQ2CQ81c7wUcVBQWLPeeJSReXITI0T+C7fyqCvA36ttUzM+e1BXh7lM5zVP6+Ld7jOyNiCpAzM7rBwvB4kAlBU2Q+NERR4V3+tV28fi2m+R354Lpx3W8MSJqOASvS3TRVLbfbkqqqboibl/0rpQ9brmNvba7tgMxkRUQHkMRc5eLiIU4v8ABK7TOcXlIjM2biUF2Gq8QOQn3OE1VV8FEEuFS23XbbflUVWEqtz70fRXrw8XMoqn9fKSDkUezxyDr5pzRcICisbPdUe+4r1ilsgrtuiInNPH9dexP6Oe9ZTendR3sldA0E7V1CNruI791qSIv/7a8b92YvOmpIuWO32C25HVZLbZNm05vz34eAOJUXbwl4v5+NV9m/0dbONMFqIzYrost5py1NSQMFFxjhSVwtn5N0XjTZF5bKmyVc9mWaacXTVlt2/LZL1h7WjciYXbonfRY0b+7/Hvy5it4yPMLZjFxx+2XBmSbmR3FbZFJoRUQdRh1/icVSTYeFg03Tdd1TltuqaP0TvosaN/d/j35cxWa1Jxm93+/wCAzbRC69my5Cc6cXWAPUsLAltIexKil4brabDuvhb7bIqp9a0XctesOkl7kXWJZdUsQuD9ijDMujUW9xnTgRyRFF19BNVaBUVFQi2RUVOddyzakad5FDeuGP57jlzix7glpefh3Vh5tucpIPcxEBKiPcSoPVr4W6om1eYs90iyPF9DYLkzFosVbFphktsuiq8yqNSpKxnEbJQJVJDJt0lUOJN0Xdd1TfZLhg+p16bn59a9KX7SUBzFgh4ws+AEq4N2uWbrzrRtvlGFCbcQGUddbJUbRD6rklBZLvrTprZc+x/TSZllr7/ZI7MYhRRnx+ProwAbjRApofHsabCgqvj32rMXjULAceyO14df84x+2X++b967VMubLMydsuy9QyZIbuy/3UWo3heIamQ9QbPmd80+mRWJmU3+XIZbnwnDt8SZGjpHee2e2LmwoGLSuEhryQg8OsnqNiWdydXIN0xHFr2sS4LbhuUwX7RJscpqO8R7XCPLRJzTjSEZMlBVeIyBXFRB2QKnCz3BbnlU7BLbmlil5LbGhkTbMxcWXJ0VottjdYQusAV4h2UhROafXXBZtTdNsjbvb2PahY1dG8adcZvRwrtHfS2OBvxhJUDXqSHhLdD2VOFd/Etec8S0G1Qh5+Vvu8vNDt9uumRXWFdHp9hCytLce6OBWBZjLdXXdpAobb7gAKgpI45wgi4PE+j3qu5i8223y15e/NsGOwLJFayC446kK6DEmsyCixAt0UHCjOJHURdmmDgo9srSKpkgenNPdWcK1TevfyGurV1hWOUzFK4xX2X4ctXY7b4nHdaMkcDhdFFXl4SEnNE3XcalWitjyaNkWoOX5Fp6eIDll4iXCLCekxXpJgFvjsGcjuU3Gxd42iHZHDThEV4l32Sq0Eq6J30WNG/u/wAe/LmKqteX+jJpPnlx6NulFwh9JvUq1x5WD2J5qDEgY2TEUCgMqLLavWk3VAUVBRXDM9kTiIl3VaX+xvUX0sdVfw3Ffg1BVaVKv2N6i+ljqr+G4r8Gp+xvUX0sdVfw3Ffg1A1k/rF0J+8CZ/pW/VVa8v6saT55FzzRhh/pN6lTDmZxKZZeegY2hwzTG70avNIFpEVNRAm1RxDDgdNUFDQDCl/sb1F9LHVX8NxX4NQbri2oODZxIu8PEMttV4k4/Petd2YhygcdgS2jIHGXgReJs0IS5Eib7bpunOtFxr/drl/z68/mMivD8foB9JjUHpcZZrVF1YyLTqys3VY0bI5DkMb9eW2BBo3wj25tiMLTyskqK4AKoECmDqqSl7YwmM9Cs0mHJnvznWLvdmnJT6Ajr5DPfRXDQBEEIlTdeERHdeSInKtHAbqvl9VLF76fn9GfrXtRYsmdp9k8KFHdkSJFmmtNNNApm4ZMGgiIpzVVVURETx1sNKvztVI2POeHYHn+HXPTq45VkOYZtaRsiMRIM2JEjnYLssTYSPuKIyXVE0rrHG8S9USpuqqe4ybH9P8AJbhZM0ODpudm+UuDzYs9iz4Rc7LKO5de0StSZMl5x25yRRxzhloI8e7hAp7lwe5axGS5hiWFw2bjmOUWixRZEgIjL9znNRW3Hz+Y2JOEiKZbLsKc18lRTbjvlJFcvO+TaV2nFrhkNrt2m7/7PFvdkuV7slssrshi5MdzPhIIYjIr3UvXdzG8AAanwbkJLyXXI+EWeLZ7f8ttHMnuODpcMrODYix+TPdjTZE1DgSFigjhtbtdb1TpAiMcaIpNeJPUEzUfTy3PWWPcM8x2M7kqoNlB66MAVzVdtkjIpfvvnJ8zfxp9dC1G09DKGsILO8dTI3lMW7Qt0Y7tNRFCJBY4usVUFUVdk5IqLSaKSK5ebbno3f73abndM8xS43fKoGOYixFmGDj7zE1p93upyK8Cbi+gmqOOtKhcKpuu21WDS7DWMLi6i47Y8Z7y2bv485aYUeIrEbqnIMdTKO2iIPCTyuqvAmymp+Xetwj6l6cy5d8gRM/xt6TjA8V8ZburBOWsefOSKFuwngl8/bxL9VdORrHpDD7z916qYex8onCZs/WXyKPfFwXEbII+5/viQ1QFQN1QlRPGtdimmJzzJqqnY89WPRy8YphUf9n+Dz7Rf75pa5HvD8WOUSVNuQEx1bb7u4L3Ugm+IKZIYoqoioicu1jGIspf4U7S7TbIsZwQb3bnItrGxvWg2pQW+cMuS3EfbbKOiq5GBXTEQNwd91+ctuha6aUySy5ZWcWS2sYPcBtl6kzrnGZZjOkAEKkaubAKqfB4fCvGBjtyrPPagYHGds7EjNrA05kIiVoA7kyJXFC24VjopfvkXdNuDffdK5FFPdJNVXfDytbsEyANMMusuA4e3AZOVaDuEtdP7jan7iw3J4pbM23OPit2e6lCV6RHMEko4TY8WyCtp6MlhnWDCbkw4yca3SLu8/bIg4o/jcaMwoAijGt8iQ89HZVxDJBc6teIjVAQVFVr9K7Tbimc3JrmYyKUpUjwyelH/B6/83vH5lJrcKh+I6aZpkFsl3e09IXUDGYki73Xq7Xa4Vgcix+Ge+K8BS7Y88vEqKa8bpeES7bDsKZv9jeovpY6q/huK/BqxsV+fX8Z9WnY/Kp+Eeiq1Kulj9FjWT7v8h/Ln6fsb1F9LHVX8NxX4NWMyfo65LmmNXbDss6TuqVysd+gv2y5wjh400MmI+2TbzSm1aBcBCAiHiAhJN90VF2WoEq0UpSgUpSgUpSgUpSgUpSgUpSgUpSglXRQ+i9pF/5Hsf8A7FqqrUq6KH0XtIv/ACPY/wD2LVVWgUpSgUpSgUpSgUpWmZpq9gmAXBm0ZBOuTk55pJBRrVZZ10djMKqoj8gIbLqx2dxJOtdQA3EvC8Fdg3OlcECfCukKPc7ZMZlxJbQvsPsOIbbrZJuJiSciFUVFRU5Ki1z0Cla47n+Mt3C5Wpt64Spdnd6ic1DtUqSrJ9zpIQV6pskXdpUUdt+IlQE3NUGvu2Z5jF5usqyWuXKkzIMxIEsAgSOGM+scZCA6fBwt/ujBdyVE3JA34vBoNgpSlApWNsuR2bISuIWeZ3QtqnOW6WnVmHVyAQVIPCRN9kMeabpz8dZKgUpSgUpSgUpSgUpSgUpSgl3SPkyImntqeiyHGXCzrCmSJs1FVbdyW2tugqp/ZNszAk8SiRIu6KqVUagGQa141mVszqxZZp+8XyDyu39xxnLkTYXRYdxiOMTW3GxQg6mWjSk0SLzAULiBxN6dE1d0/nZs5p7GvTxXltw2E4rfJGI5IAONyO3MVtIzj4BuRMg4rgiiqooiLsy7xuNKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoJV0TvosaN/d/j35cxVVqVdE76LGjf3f49+XMVVaBSlKBX5ExXL6w4L8OX4KLsSSHCUxX+0iKXLbb608lfrtURmdC7o3T2uok4HOVv+4OSXQE/yGSlQ3rWtjLPJ2Mn4s9IHIbo/lr0Xu1QYjIiCDbm47qm6rulSQMjuzKqjU93gVfmkSrtX7rSv6NvoXTXCelaOG6ZruSlkt35r/wDNV8Rv6NPoTRJLUtjRQesYMXA48iuxjxIu6biUpUVOXiVFSrtuu3RRFGSaLsQ/Ovo+dFy95PZLZqJnE2SzbJSJJC3IPBIJj604/BBS8aEqF4K7oiKqKntbA8bxHG8dKbhlht2NsL4Sk00jsh9UXxk54Rnz35kq16Ye0B0lfa6lzFSQNttguEoNk/hs4m1c1t0N0vtMYIdvxx5pkN0Ee+Ustt/H43VqjNnSqmqre7N6mdsvJ2QXVO+TU+Xc1fEEXj6xNtv4oq18zpRXeGiDfElRuJFSM4qKibf3dh32/wA69Vv9H3SCQhi/h4GhookizZOyov8A+pXXh9G7Re3goQsOJoV8g3OZ73lXZtzOyXNZT3PKky6I9HbtkyJHFhjbq21c/d8HlXYUTn9flrUct0+sWQ7Ns2q2k05vtIYFS2/huRKqf5V7XkdGvRSVxK/hilxeP/vKWn/o7XxH6MuiUXnHw1xvflyus331c1U95N2me5+YeYaAtR5yzSkyphgClHYEN12TyIm3/wBb1qV3VuxWqKy2s6NPcfI3nQJwERBXwQJCRN9lRfFy3Wv1zLo76PmvEWJmq8uffOX72sNfeiN0eclVsr3p6klWt0Fe+s0PH49+F5N/8aq3sBrNsShmmic8t78lorvWE26NzluukyTjLZOcBtF9ab8k225cuWyL/Bfb/wDRhwo8B7U2NHHZBGyIqqK8Xim8iVV3VdtvqTn4k3q7ROhD0X4QvDG0yUUfNHC3vdxJUL6xVZC8P/5dq37TfRjTXSJbkWnmNJaVvCsrN2lvv9crXHwKvWmWyp1h7qmyrvz32SmHwddm5FU5IqaNGc2A6J30WNG/u/x78uYqq15q6PWtuL4NoFpphWU4bqrCvWP4fZrXcY37K8nc6iUxCabdb4wgEBcJgSbiqiu26Kqc6oHaW0683NVfZPlXw6tFIqtKlXaW0683NVfZPlXw6naW0683NVfZPlXw6gqtKlXaW0683NVfZPlXw6naW0683NVfZPlXw6gqtKlXaW0683NVfZPlXw6naW0683NVfZPlXw6gqtKlXaW0683NVfZPlXw6naW0683NVfZPlXw6gdE76LGjf3f49+XMVVamnRktN1sHRt0osV9tku3XK3YPYokyHLZJl+M+3AZFxpxskQgMSRRUVRFRUVFql0ClKUEq1k/rF0J+8CZ/pW/VValWsn9YuhP3gTP9K36qrQKkuNf7tcv+fXn8xkVWql1xt1yw65TwO0XCdap0x6fHkQIpySaJ41ccbcbbRXN+sI1QkFUVCRF2VOd/A1RE1UzvnJUxdMzEVQ71KxHyljfYeT+rVx9xT5SxvsPJ/Vq4+4rSylRzhl6iHSDyyw6dZPiWc5NMs70FI1zsyW64XmBAI3JItbPt92Oti6go2QGLak5wOrwga7itY+Usb7Dyf1auPuKfKWN9h5P6tXH3FcqomYyeoqiJzeTWdE9TLli2PdyW/MUgZFg9kscu3WiVZYjEM46uGQzVuUZyQy2nXCQlEEnEUS3bQkFaq56TX4J91nMY+0kibqNAyDunrWVcOI1FjslIVd0XdEbMduRKich2Wq38pY32Hk/q1cfcU+Usb7Dyf1auPuK8RZyepuTLyratANUksF2xm7W/LLi9acRu9ggvXK42ELbLclkC8EMYzAzDA1BDIprraiu3+1VVNNw1z081ayW7XG0YbZrq3ap2PRIjTloSyNMynWTcI49xdmgUvgRFFGUjcIopnxGG/Gl6+Usb7Dyf1auPuKfKWN9h5P6tXH3FNTsy2mt25otecLz+35nOyuLp9Ovca1ZmzkbERiZBE7lHctCQ16jrnhEXmXU4lR4mhVOYmS8qyGkuluUY1lOP5BfbCxCBi033dlqQDg2051yaktw02JeLhbRRUgRQ3BURdlTes/KWN9h5P6tXH3FPlLG+w8n9Wrj7iu6rbm5rM4yZelYj5SxvsPJ/Vq4+4p8pY32Hk/q1cfcV7yl4zhl6ViPlLG+w8n9Wrj7iv6N8lS17ntGLZDKlHyBt60SYbe/1k6+AAKfWu6rt4kXxUymN7u/c2nSj/g9f+b3j8yk1uFYTDbA7jOORbRJfB6QJOvyXARUEn3nSddUd+fDxmW2/k2rN1h4iqK7tVVO6Zn1alqmabdMT4QUpSokhSlKBSlKBSlKBSlKBSlKBSlKBSlKCVdFD6L2kX/kex/8AsWqqtebdAtaMawDQzT3Bcsw/VKHe8dxe12q4xw0uyV8WZLEVtt0EcagE24iGJJxARCu26KqKi1vvaW0683NVfZPlXw6gqtKlXaW0683NVfZPlXw6naW0683NVfZPlXw6gqtKlXaW0683NVfZPlXw6naW0683NVfZPlXw6gqtK1/Cs4suf2p282KFkEWOzIKMQXvHrhZn1NBElUWZzLLpBsabGgqCqhIiqokibBQKkl9bzzAtTL/lmO6Z3TNYGWwYDCLbbjBZct0iMjo7PjMfZRI5C6JIrKuGhI5+75opVulB5jznSbUW/wCcW683fTi3XnIHo1lS05VbpDDUXD3o7ynPRpH3UlNA8K8Kdzg4ryeA9wCiVr7PRtzy3Y467jOJxbPkd3x/JYN7mMymWnrgrtxbehR33myUjQmOuACXiRlHFTwPFXpXU/J5+FacZRmFqZjuzLJaJdwjhIEiaJxpojFDQVRVHdE32VF28qVoWVdJOz4ll8HT244ldTvd3tT0+1GEy2C1PcajG8Tbcc5iTEH92QdaTAs8WyK4m6KoSBdCc2kwr8uIaK/Iq1T5c523WHu23B3K05jZQwHgjvmw1vJXh4WzUU341XZVVMlA0A1Bt2Y5BNxvEo2PyLpd37g1fGHoraKTuOLDB0laNXlIJfFuqhvufGPEiqqZyy9OPTmBB0+teowhZ8ky+z2u4z4/fK2sBbim7CyvUPTEkPgZ7qncwSFAdlc4Kz936U8BbBkk3HNPMoOVbbPfJ9oemtQwi3R+1ukzJaDhlcY8DiCv71GkIF3AiVFRAnVp6PV4vE61RS0OiYxh63e0d+salv291icbLE0J1wcZYdcZdB/r2AVTXrnkFVdbTasZqh0fdSb5pbjmmts0thTYtoj30bW4zHs78iySUlEVsFt2eZBFirH2BSisnIHZpBJlBVateAdJC1ZnmUHTlzDb+zfFtcOdc3gWEceA6/GR8RdZblHKBtUVRF9WVjqfgI8pcqsdBP8AR/GshxqJk6ZFBKM7c8ikXBhCeBxXGTZZFDVQJdlUgJNl58v4pVApSgUpU/ynXDC8PvsrHLtZdQJEuJwdY5a9Pb/c4pcYCacEmJCcZc5EiLwGvCW4rsQqiBQKVKu0tp15uaq+yfKvh1O0tp15uaq+yfKvh1BVaVKu0tp15uaq+yfKvh1O0tp15uaq+yfKvh1BVaVKu0tp15uaq+yfKvh1O0tp15uaq+yfKvh1BVaVKu0tp15uaq+yfKvh1O0tp15uaq+yfKvh1Bgc60EssjFrrHumo4WB66Zq3fWLmbINi2sqZGTvcSG5wuo+YNtJzRVcNtRHjEUXO23RO6Qc6j3l3NGXcXt98mZPAs42vhlt3KS26DvWTOuUXGE690hbRgTQlTdwkThXTNWdV7Dqdjtnw7DsT1Jfub+YYpMFJmnGQwGAYi36DJfcckSYTbLYgyy6aqZong7Juqoi+iaBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKCVdE76LGjf3f49+XMVValXRO+ixo393+PflzFVWgUpSgUpSgVFpmoer93t991Hw1MTHEsckz2Es02BJduV4CEZtvmEwHwbhkptuoALHf3QBVSHj2C01Jblo5mondsbxTU2LZ8JyGTJk3G2nYlfuTCySUpIQZqSAbjgZEZfvY75CTh8JInAgBlGtdcPftM69MQrq5GgTbRBJRZbRXDuQxyYIEU08FElN8W+ypsWyLsm+lYz0m7vKgjHvukGWyb/Pvd/t9rtdpbt5OSmLdI4FJTOb1QFwKm6uOAhGBoKeE2hZDIejxeLhfZSY5n8azYtPnWS5ybR3l659JFtNjqwako+IgwbUcAIFaIkJEJDRNwXDXHRLVuz5lZ5uA5xZ4jcabklxauM2xFJZhjcXmXUjPxxmNHJXiV9RcbNpBUG+IV2VDDd2ukHgsmwXfJYca7PwrLYIeRPqMcBM48lXhFsRI0VHRVhxCEuFEXZN157c+nGpeQ5jkGodru+GzLdGxC9pbre8qx17ua7kZe3TgkGvHu4q+ELacJtptxIe2i3XotXYLS9jWI6oLbLPdMbi49eAnWdJkmT3M6662+08LzSMqRPuo6JA4hCqIHVqnEtRwzB5+JZLl93O+R5cHJ7gzc2oqQibdiujGaYcQnesJHRJGAIURsFHckVS3TYNUuXSExh+xWm6Y+E5Vu1ugXZHDgg+MViTNZig28CPtqLpE44KbEqD1TheFwoJ464dIhx7UDH8fsmJ3JnGpd7uNqn5HcY7Yw5HccSS4+kVQe60VbejoBE8yIGiH1anspD923o2RbZbsut7WYPOpkl+h3SGrsEFS1wo8sZYQAQSRTDrikkhku6dftsqAKV/GOjzdByGKEjO4x4bbbvc7vBsjVm6uSJ3BmSElt2Z16oYIcpwm+FoFFFUSVzwVEO1bek1iMq2T7tdcQy2yMtW4LvahnQ2CO+wnHRZZehiw84u5uOMijb/Uup1oKQCiqqbfp1qTD1Dj3NssbvWN3exy0hXSzXkGElxHCAXG1Io7rzBibZiYk26abLsqoQkKRvDuhdYcTst4s8SXhNsJ+HGh2mbjmAQbVLbWNIB9iROeQnDmvIbLPFwqw0fCS9UhEhDW9MsAv+IOX2+5nlkbIckyWY3Kny4VtK3wwFpoWmWmI5PPE2IgCKqk6akZEu6IqCIbzSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKCVayf1i6E/eBM/wBK36qrUq1k/rF0J+8CZ/pW/VVaBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBStfy6blUOO0WOQgdBVXuh0WkfeaHbxgyTjaF/PjVUVERGz35S3IHr3d8/iQ4kvK5TQFCZFVagNEXCKyT8BwQUV/dtFsQoqLxIqbcIqFypWgZ5k863XR22M5IVlJq3LJgiDLTjtylESiLII4JcSIojuIIhL1ic0RK+cPi8cnJc7u7p93E45F4lBlVjNMAiGLZ8HFwoaHyVVReFFXdd1UNpzDGIGa4peMPur0hqHe4L9vkHHIRdFt0FAlBSRUQtlXbdFTfyLU7Ho04eGZScwbyTImxlXIbw5bGyiBEKakBYJPEQx0fcUo6oOxukIqKKCBz3x+Pv3jG7JcbjGvjsYGIcWZLNYkXifny9y4n3FAf3YI4Cqqqiomy77Jwr27TmeVXJo4KZY31bEuS8/co6sSl7jYjATnAaR22y3cdHhLq+X/AI08Yfdh6N9nxc7ImP6lZ1b49rt0G1zo0adFaC8x4ar3Kkohjo4BAJKClGJgjHwXFNK77fR6wlu1BZluF5OOMa/xfCfa4lC7vdbJ3VG05iS7N/UnzuLx1i3ssz+1QxKTkLLxPQLest+W2zGZt7sgy8PjFouFOANlI0MUM0XhQfBr7l5tkMO0QO7cyYF11x9xtyKrJPy2d0FpWlfjtNSdj4uIWkbIhUFFV38IOzC6O2Os5fiuX3fMMkvh4Y0PemHchgONMSUjdzlJBwYoyGiNvkTTToMKvhdVvzqr1pnfrJIN1lnIeI7FH4ylzp8UIYxlRPC6o1LicEf7KEyqLuv75dtq1aZfp16xZ5/Jr3EjgzOjMnDuQ9zR5MdPDEpBiBK11489lRQTZA4d+JKCuUrz7IG4FbLc1e4tlYtxRpk+2WeYy6bUlx19UaYYbRQXjRtR4OW4daioG/zd0ayedPcvEFjInoNqiQZDkKYjSOk4QtoDgiSCpOBHNd1MfCJVTmqCqkFOpUBZCQdukhCPHbRjcqdBgyZTLxu259G23CedNzZvrEMurAl3FCVOBTXmtVrTsW0xCCbNtZgtuK44DLAmDKirhKhtgaqrYGmxoG+yIWyUGyUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSglXRO+ixo393+PflzFVWpV0TvosaN/d/j35cxVVoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoJVrJ/WLoT94Ez/St+qq1KtZP6xdCfvAmf6Vv1VWgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgVrGp2XSMB07yTNolvbnP2O2SJzUZx1WwdNsFJBIkRVFFVE3VEWtnrE5XjFozXGbpiN/Zcdtt4iOwpQNuk0atOColwmKoQrsvJUXdKDz7qRqJ0pNLUx12VkGm2ULee6JD8WDh8+E62xFYWTIBpTujqOOE0DggqoKIaCqoSKqJsma6o6sZTlUGwdHp3FX4DFthXO9XS82x6a3HamGisK2DcuNxKjIOukCkpbE1snhLWzYxog9aMjtWS5bq3mucPWFp5u1MX0LW0zEJ1vqzcRIMKMThq3uP70jREJVRN+dZXTvRzD9K8XuuKYR3bBj3aZKmuPk6LzzTj3IRbUxUUBoEBtoFFRAGwHZUTmEgxvpOZJp/pZF1M6SNyxNuJkTduLGlszTVnSc/IYN12Kq3C4G0JN8Cr1rrzIKn8dt89G6ZemE7T236pW6z3ydjL1zds91uEJ+3SY9jlBtskt5qWTSgSqIi6wbzaqY7km6VtS6AWFNO8TwNrL8lal4QTTthyNs4iXSI6AE2jn+79zGqtGbZCbBAQku4qvOsHk/Rdt2c2GDjOeawag5LbGri7c7lEujtsej3l0tkbCUz3EjSMtIicDLAtN8ScZCZ+FQdbJukPnEG26cXqw9HzOXQza4uMSLZMK0M3CMykZ51sFE7iDbbx9WJohESCAuCfA5wivV1TzjpMW3LrREwRnDbJaL88xAt8e/2J24zHJZQZUl0ScjXNlsOEo4NbcKjuakjhImy7aGhJDp/juEHq3nT8/FJQS7Tk8g7c9dmSFs2kElOGsZ1OqcNvdxgiVF3UlNEKszctKYtzTCetzTJhLCJ6XFlwpDD7lyd6lxle6zeZMiRRdc/wBkraopclREREDVpGvzttZS33bDL4LtvGNb8iyOHFjyLLYrs80Cqy8HdQynQA3W0ImQcAENON0OEyHEBrNqGeCtY8i42mqx5QuIm13vf72hIQutKX3OkjrVZ7h/7Rw9fvzQVJF322u+6CWO+5FLuZZllUOyXac3c7xjEWTHS2XSUCBs46pMlJBF6tpSbZfbbc4PDAuI0POBpPhwapO6wBFfTIXrWlqIut/cICFv1qN7bdco7Ap+PgFB8VBjcM1PmXa8Zxb8ts02wBh5sk53fGjNocYmiLukXI8yQhtn1ZmKGjTgDsJAq86m7nSvftuf9bmWH3bDNPAw+bk/fTIIEcHJLbDzIi8y5HmuqgEDyKrD0dp4VUN+ZKI7tZdBHYGU37I77rDm2Sxcnj9yXazXSPZhhSmEbNsG17ngNPCgC4SJwuoq8uJSrEXDoq45fupj5ZqXnd+t0a3TLKxAlyoTTTVukI1/2YTYitup1ZMNGD/Wd0IQJxOknKgxmDdNrRzUCJfO8Sy3bpZViIFpi3G1XKTcFlOK1GFg4Ex9jiNxODgddbIPnOIALxVlcx6QFxiabZBkdqxG9Y1fsclx414g32zFc3bLHdVCWc7GtjzndjKM7ubRn132VCIFA0HKM6ALJt0qJlusmomUSSGKtum3CZCYctTsdzrGn47cOKwwrqHsqm826pInAXECkC9iJoasK1SgZ1azxMinzguEzJ0kwhuEk22jaZbNoYqQ+pbE/BZSOjakKEQkSkpBPLNrFq9qKzhEnSvUzSK6QcrG7OBc49jn3CM+EUWiaTYZzJxXS4yRxokcVouW5KK79XM+kbmkKx4FkEjOtNNLYl/lXW0Xx7MIjk+LFuMJXBIWJKT4Qk2TjLgjxChEiiWwruFbnK6MrByLZcrXrRqDaLrCenyplzhd6O6LnImC2L7z/WQDACUGgAUYFkRQdxRF51nI2gmPWefgz+K5TkVhgYH3QsS2RXIr7M8nxVHjluymHZDpnxGpGLokRGRKqku9Bi7DrNlcxNMmr/hc+3rmiuNTJzcNooaPiw+YtCjkpuUx1iM9cJLHeHgVAJRJeJEjpK2aDe73bbjprm0W3YvdktN9vZsQSg20jFsmXj4JSvONOC82u7TbhNou7otJWW1J0ZueouR2jIWdZ84xdLE8kqDCszFmKO3JRt1tX1WXAfcIlbeMVFTUPEqCi860Kw9H/P7vqBmk7UPIHIeIZFdGbgVus2Qi+N7FphhkAnsuW1s2N0Y4ySLJAT41bMTBOYd6N01tC5mq7WksPIGH5r91KxBOau1sNvviKkJR1ipK7vTYxUOtWKjO/wD9psqLW8YBk2czM5znEsxn2O4N2J2G/bHbXa3YRJGkg4YtPI7JeRxweBEVwVbEt1XgHxVj7doHDseRJccc1Oziz4+t3cvjmKwZURq2OS3DVxxVc7m7sRs3SVwmRko0pKqKHAqgv3jGil5xnNrvnBa557dHr2Aty4U2NZEjEgAYs7dTbm3E6pDVR8PmqJx8flDrW3XB9nCs9zbJMLyCP8h577UuzpGhBObYbYae3QgnOsPL1bnWcSOtqqLw9WhJsvRvHSowDEscvuRahWe9YaNm7jNqNfTgsOXBmWZBEdZMZJNNi6bZjtIcZJvgJXRaFOKuKP0Z5g2vMrPcekBqTco2csPN3UZDFgFetcabZJ9tWrYHCfVNCCJzDZVXg4tiTtZD0abHlFykXe9ai5m7LO3WuHFdacgMlAkW9wnY05km4or16OG4RCamyaOEJNKC8FBltDOkDgHSBstzu+DyFRyyze4LjEKdBmFHdUEMP38F+RGcEgJFRW3j25iWxCQpS61XAsHnYWxPW7ah5VmE24vC67Nvz0bibQRQRbaZissR2hRE3XgaFSVVUlJdttqoJV0TvosaN/d/j35cxVVqVdE76LGjf3f49+XMVVaBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKBSlKCRa+y3rJfdJcxcst9uNsxvNn5t0WzWWXdZEeO5j94ig4seI248Q9fJYBVEF26xFXZN1TsdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSgdpbTrzc1V9k+VfDqdpbTrzc1V9k+VfDqUoHaW0683NVfZPlXw6naW0683NVfZPlXw6lKB2ltOvNzVX2T5V8Op2ltOvNzVX2T5V8OpSg7vRqsl3xro5aV45kFtkW+6WrCbHCnQ5Dag7HkNQGQcbMV5iQkKoqL4lRapFKUClKUClKUClKUClKUClKUClKUClKUClKUH//2Q==)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Define the conversion function\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Model components are PyTorch modules, that can be converted with `ov.convert_model` function directly. We also use `ov.save_model` function to serialize the result of conversion." + ] + }, + { + "cell_type": "code", + "source": [ + "warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)" + ] + }, + { + "cell_type": "code", + "source": [ + "from pathlib import Path\n", + "\n", + "\n", + "def convert(model: torch.nn.Module, xml_path: str, **convert_kwargs) -> Path:\n", + " xml_path = Path(xml_path)\n", + " if not xml_path.exists():\n", + " xml_path.parent.mkdir(parents=True, exist_ok=True)\n", + " with torch.no_grad():\n", + " converted_model = ov.convert_model(model, **convert_kwargs)\n", + " ov.save_model(converted_model, xml_path)\n", + " del converted_model\n", + " gc.collect()\n", + " torch._C._jit_clear_class_registry()\n", + " torch.jit._recursive.concrete_type_store = torch.jit._recursive.ConcreteTypeStore()\n", + " torch.jit._state._clear_class_state()\n", + " return xml_path" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### UNet\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text-to-video generation pipeline main component is a conditional 3D UNet model that takes a noisy sample, conditional state, and a timestep and returns a sample shaped output." + ] + }, + { + "cell_type": "code", + "source": [ + "unet_xml_path = convert(\n", + " unet,\n", + " \"models/unet.xml\",\n", + " example_input={\n", + " \"sample\": torch.randn(2, 4, 2, int(sample_height // 2), int(sample_width // 2)),\n", + " \"timestep\": torch.tensor(1),\n", + " \"encoder_hidden_states\": torch.randn(2, 77, 1024),\n", + " },\n", + " input=[\n", + " (\"sample\", (2, 4, NUM_FRAMES, sample_height, sample_width)),\n", + " (\"timestep\", ()),\n", + " (\"encoder_hidden_states\", (2, 77, 1024)),\n", + " ],\n", + ")\n", + "del unet\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### VAE\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Variational autoencoder (VAE) uses UNet output to decode latents to visual representations. Our VAE model has KL loss for encoding images into latents and decoding latent representations into images. For inference, we need only decoder part." + ] + }, + { + "cell_type": "code", + "source": [ + "class VaeDecoderWrapper(torch.nn.Module):\n", + " def __init__(self, vae):\n", + " super().__init__()\n", + " self.vae = vae\n", + "\n", + " def forward(self, z: torch.FloatTensor):\n", + " return self.vae.decode(z)" + ] + }, + { + "cell_type": "code", + "source": [ + "vae_decoder_xml_path = convert(\n", + " VaeDecoderWrapper(vae),\n", + " \"models/vae.xml\",\n", + " example_input=torch.randn(2, 4, 32, 32),\n", + " input=((NUM_FRAMES, 4, sample_height, sample_width)),\n", + ")\n", + "del vae\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Text encoder\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text encoder is used to encode the input prompt to tensor. Default tensor length is 77." + ] + }, + { + "cell_type": "code", + "source": [ + "text_encoder_xml = convert(\n", + " text_encoder,\n", + " \"models/text_encoder.xml\",\n", + " example_input=torch.ones(1, 77, dtype=torch.int64),\n", + " input=((1, 77), ov.Type.i64),\n", + ")\n", + "del text_encoder\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Build a pipeline\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def tensor2vid(video: torch.Tensor, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) -> List[np.ndarray]:\n", + " # This code is copied from https://github.com/modelscope/modelscope/blob/1509fdb973e5871f37148a4b5e5964cafd43e64d/modelscope/pipelines/multi_modal/text_to_video_synthesis_pipeline.py#L78\n", + " # reshape to ncfhw\n", + " mean = torch.tensor(mean, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " std = torch.tensor(std, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " # unnormalize back to [0,1]\n", + " video = video.mul_(std).add_(mean)\n", + " video.clamp_(0, 1)\n", + " # prepare the final outputs\n", + " i, c, f, h, w = video.shape\n", + " images = video.permute(2, 3, 0, 4, 1).reshape(f, h, i * w, c) # 1st (frames, h, batch_size, w, c) 2nd (frames, h, batch_size * w, c)\n", + " images = images.unbind(dim=0) # prepare a list of indvidual (consecutive frames)\n", + " images = [(image.cpu().numpy() * 255).astype(\"uint8\") for image in images] # f h w c\n", + " return images" + ] + }, + { + "cell_type": "code", + "source": [ + "try:\n", + " from diffusers.utils import randn_tensor\n", + "except ImportError:\n", + " from diffusers.utils.torch_utils import randn_tensor\n", + "\n", + "\n", + "class OVTextToVideoSDPipeline(diffusers.DiffusionPipeline):\n", + " def __init__(\n", + " self,\n", + " vae_decoder: ov.CompiledModel,\n", + " text_encoder: ov.CompiledModel,\n", + " tokenizer: transformers.CLIPTokenizer,\n", + " unet: ov.CompiledModel,\n", + " scheduler: diffusers.schedulers.DDIMScheduler,\n", + " ):\n", + " super().__init__()\n", + "\n", + " self.vae_decoder = vae_decoder\n", + " self.text_encoder = text_encoder\n", + " self.tokenizer = tokenizer\n", + " self.unet = unet\n", + " self.scheduler = scheduler\n", + " self.vae_scale_factor = vae_scale_factor\n", + " self.unet_in_channels = unet_in_channels\n", + " self.width = WIDTH\n", + " self.height = HEIGHT\n", + " self.num_frames = NUM_FRAMES\n", + "\n", + " def __call__(\n", + " self,\n", + " prompt: Union[str, List[str]] = None,\n", + " num_inference_steps: int = 50,\n", + " guidance_scale: float = 9.0,\n", + " negative_prompt: Optional[Union[str, List[str]]] = None,\n", + " eta: float = 0.0,\n", + " generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,\n", + " latents: Optional[torch.FloatTensor] = None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " output_type: Optional[str] = \"np\",\n", + " return_dict: bool = True,\n", + " callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None,\n", + " callback_steps: int = 1,\n", + " ):\n", + " r\"\"\"\n", + " Function invoked when calling the pipeline for generation.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts to guide the video generation. If not defined, one has to pass `prompt_embeds`.\n", + " instead.\n", + " num_inference_steps (`int`, *optional*, defaults to 50):\n", + " The number of denoising steps. More denoising steps usually lead to a higher quality videos at the\n", + " expense of slower inference.\n", + " guidance_scale (`float`, *optional*, defaults to 7.5):\n", + " Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).\n", + " `guidance_scale` is defined as `w` of equation 2. of [Imagen\n", + " Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >\n", + " 1`. Higher guidance scale encourages to generate videos that are closely linked to the text `prompt`,\n", + " usually at the expense of lower video quality.\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the video generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " eta (`float`, *optional*, defaults to 0.0):\n", + " Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to\n", + " [`schedulers.DDIMScheduler`], will be ignored for others.\n", + " generator (`torch.Generator` or `List[torch.Generator]`, *optional*):\n", + " One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)\n", + " to make generation deterministic.\n", + " latents (`torch.FloatTensor`, *optional*):\n", + " Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for video\n", + " generation. Can be used to tweak the same generation with different prompts. If not provided, a latents\n", + " tensor will ge generated by sampling using the supplied random `generator`. Latents should be of shape\n", + " `(batch_size, num_channel, num_frames, height, width)`.\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " output_type (`str`, *optional*, defaults to `\"np\"`):\n", + " The output format of the generate video. Choose between `torch.FloatTensor` or `np.array`.\n", + " return_dict (`bool`, *optional*, defaults to `True`):\n", + " Whether or not to return a [`~pipelines.stable_diffusion.TextToVideoSDPipelineOutput`] instead of a\n", + " plain tuple.\n", + " callback (`Callable`, *optional*):\n", + " A function that will be called every `callback_steps` steps during inference. The function will be\n", + " called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`.\n", + " callback_steps (`int`, *optional*, defaults to 1):\n", + " The frequency at which the `callback` function will be called. If not specified, the callback will be\n", + " called at every step.\n", + "\n", + " Returns:\n", + " `List[np.ndarray]`: generated video frames\n", + " \"\"\"\n", + "\n", + " num_images_per_prompt = 1\n", + "\n", + " # 1. Check inputs. Raise error if not correct\n", + " self.check_inputs(\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt,\n", + " prompt_embeds,\n", + " negative_prompt_embeds,\n", + " )\n", + "\n", + " # 2. Define call parameters\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)\n", + " # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`\n", + " # corresponds to doing no classifier free guidance.\n", + " do_classifier_free_guidance = guidance_scale > 1.0\n", + "\n", + " # 3. Encode input prompt\n", + " prompt_embeds = self._encode_prompt(\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt,\n", + " prompt_embeds=prompt_embeds,\n", + " negative_prompt_embeds=negative_prompt_embeds,\n", + " )\n", + "\n", + " # 4. Prepare timesteps\n", + " self.scheduler.set_timesteps(num_inference_steps)\n", + " timesteps = self.scheduler.timesteps\n", + "\n", + " # 5. Prepare latent variables\n", + " num_channels_latents = self.unet_in_channels\n", + " latents = self.prepare_latents(\n", + " batch_size * num_images_per_prompt,\n", + " num_channels_latents,\n", + " prompt_embeds.dtype,\n", + " generator,\n", + " latents,\n", + " )\n", + "\n", + " # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline\n", + " extra_step_kwargs = {\"generator\": generator, \"eta\": eta}\n", + "\n", + " # 7. Denoising loop\n", + " num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order\n", + " with self.progress_bar(total=num_inference_steps) as progress_bar:\n", + " for i, t in enumerate(timesteps):\n", + " # expand the latents if we are doing classifier free guidance\n", + " latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents\n", + " latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)\n", + "\n", + " # predict the noise residual\n", + " noise_pred = self.unet(\n", + " {\n", + " \"sample\": latent_model_input,\n", + " \"timestep\": t,\n", + " \"encoder_hidden_states\": prompt_embeds,\n", + " }\n", + " )[0]\n", + " noise_pred = torch.tensor(noise_pred)\n", + "\n", + " # perform guidance\n", + " if do_classifier_free_guidance:\n", + " noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)\n", + " noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)\n", + "\n", + " # reshape latents\n", + " bsz, channel, frames, width, height = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + " noise_pred = noise_pred.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + "\n", + " # compute the previous noisy sample x_t -> x_t-1\n", + " latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample\n", + "\n", + " # reshape latents back\n", + " latents = latents[None, :].reshape(bsz, frames, channel, width, height).permute(0, 2, 1, 3, 4)\n", + "\n", + " # call the callback, if provided\n", + " if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):\n", + " progress_bar.update()\n", + " if callback is not None and i % callback_steps == 0:\n", + " callback(i, t, latents)\n", + "\n", + " video_tensor = self.decode_latents(latents)\n", + "\n", + " if output_type == \"pt\":\n", + " video = video_tensor\n", + " else:\n", + " video = tensor2vid(video_tensor)\n", + "\n", + " if not return_dict:\n", + " return (video,)\n", + "\n", + " return {\"frames\": video}\n", + "\n", + " # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt\n", + " def _encode_prompt(\n", + " self,\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt=None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " ):\n", + " r\"\"\"\n", + " Encodes the prompt into text encoder hidden states.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " prompt to be encoded\n", + " num_images_per_prompt (`int`):\n", + " number of images that should be generated per prompt\n", + " do_classifier_free_guidance (`bool`):\n", + " whether to use classifier free guidance or not\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the image generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " \"\"\"\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " if prompt_embeds is None:\n", + " text_inputs = self.tokenizer(\n", + " prompt,\n", + " padding=\"max_length\",\n", + " max_length=self.tokenizer.model_max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + " text_input_ids = text_inputs.input_ids\n", + " untruncated_ids = self.tokenizer(prompt, padding=\"longest\", return_tensors=\"pt\").input_ids\n", + "\n", + " if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):\n", + " removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1])\n", + " print(\n", + " \"The following part of your input was truncated because CLIP can only handle sequences up to\"\n", + " f\" {self.tokenizer.model_max_length} tokens: {removed_text}\"\n", + " )\n", + "\n", + " prompt_embeds = self.text_encoder(text_input_ids)\n", + " prompt_embeds = prompt_embeds[0]\n", + " prompt_embeds = torch.tensor(prompt_embeds)\n", + "\n", + " bs_embed, seq_len, _ = prompt_embeds.shape\n", + " # duplicate text embeddings for each generation per prompt, using mps friendly method\n", + " prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # get unconditional embeddings for classifier free guidance\n", + " if do_classifier_free_guidance and negative_prompt_embeds is None:\n", + " uncond_tokens: List[str]\n", + " if negative_prompt is None:\n", + " uncond_tokens = [\"\"] * batch_size\n", + " elif type(prompt) is not type(negative_prompt):\n", + " raise TypeError(f\"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=\" f\" {type(prompt)}.\")\n", + " elif isinstance(negative_prompt, str):\n", + " uncond_tokens = [negative_prompt]\n", + " elif batch_size != len(negative_prompt):\n", + " raise ValueError(\n", + " f\"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:\"\n", + " f\" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches\"\n", + " \" the batch size of `prompt`.\"\n", + " )\n", + " else:\n", + " uncond_tokens = negative_prompt\n", + "\n", + " max_length = prompt_embeds.shape[1]\n", + " uncond_input = self.tokenizer(\n", + " uncond_tokens,\n", + " padding=\"max_length\",\n", + " max_length=max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + "\n", + " negative_prompt_embeds = self.text_encoder(uncond_input.input_ids)\n", + " negative_prompt_embeds = negative_prompt_embeds[0]\n", + " negative_prompt_embeds = torch.tensor(negative_prompt_embeds)\n", + "\n", + " if do_classifier_free_guidance:\n", + " # duplicate unconditional embeddings for each generation per prompt, using mps friendly method\n", + " seq_len = negative_prompt_embeds.shape[1]\n", + "\n", + " negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # For classifier free guidance, we need to do two forward passes.\n", + " # Here we concatenate the unconditional and text embeddings into a single batch\n", + " # to avoid doing two forward passes\n", + " prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])\n", + "\n", + " return prompt_embeds\n", + "\n", + " def prepare_latents(\n", + " self,\n", + " batch_size,\n", + " num_channels_latents,\n", + " dtype,\n", + " generator,\n", + " latents=None,\n", + " ):\n", + " shape = (\n", + " batch_size,\n", + " num_channels_latents,\n", + " self.num_frames,\n", + " self.height // self.vae_scale_factor,\n", + " self.width // self.vae_scale_factor,\n", + " )\n", + " if isinstance(generator, list) and len(generator) != batch_size:\n", + " raise ValueError(\n", + " f\"You have passed a list of generators of length {len(generator)}, but requested an effective batch\"\n", + " f\" size of {batch_size}. Make sure the batch size matches the length of the generators.\"\n", + " )\n", + "\n", + " if latents is None:\n", + " latents = randn_tensor(shape, generator=generator, dtype=dtype)\n", + "\n", + " # scale the initial noise by the standard deviation required by the scheduler\n", + " latents = latents * self.scheduler.init_noise_sigma\n", + " return latents\n", + "\n", + " def check_inputs(\n", + " self,\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt=None,\n", + " prompt_embeds=None,\n", + " negative_prompt_embeds=None,\n", + " ):\n", + " if self.height % 8 != 0 or self.width % 8 != 0:\n", + " raise ValueError(f\"`height` and `width` have to be divisible by 8 but are {self.height} and {self.width}.\")\n", + "\n", + " if (callback_steps is None) or (callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0)):\n", + " raise ValueError(f\"`callback_steps` has to be a positive integer but is {callback_steps} of type\" f\" {type(callback_steps)}.\")\n", + "\n", + " if prompt is not None and prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to\" \" only forward one of the two.\"\n", + " )\n", + " elif prompt is None and prompt_embeds is None:\n", + " raise ValueError(\"Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined.\")\n", + " elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):\n", + " raise ValueError(f\"`prompt` has to be of type `str` or `list` but is {type(prompt)}\")\n", + "\n", + " if negative_prompt is not None and negative_prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:\"\n", + " f\" {negative_prompt_embeds}. Please make sure to only forward one of the two.\"\n", + " )\n", + "\n", + " if prompt_embeds is not None and negative_prompt_embeds is not None:\n", + " if prompt_embeds.shape != negative_prompt_embeds.shape:\n", + " raise ValueError(\n", + " \"`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but\"\n", + " f\" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`\"\n", + " f\" {negative_prompt_embeds.shape}.\"\n", + " )\n", + "\n", + " def decode_latents(self, latents):\n", + " scale_factor = 0.18215\n", + " latents = 1 / scale_factor * latents\n", + "\n", + " batch_size, channels, num_frames, height, width = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(batch_size * num_frames, channels, height, width)\n", + " image = self.vae_decoder(latents)[0]\n", + " image = torch.tensor(image)\n", + " video = (\n", + " image[None, :]\n", + " .reshape(\n", + " (\n", + " batch_size,\n", + " num_frames,\n", + " -1,\n", + " )\n", + " + image.shape[2:]\n", + " )\n", + " .permute(0, 2, 1, 3, 4)\n", + " )\n", + " # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16\n", + " video = video.float()\n", + " return video" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Inference with OpenVINO\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "from notebook_utils import device_widget\n", + "\n", + "device = device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_unet = core.compile_model(unet_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_vae_decoder = core.compile_model(vae_decoder_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_text_encoder = core.compile_model(text_encoder_xml, device_name=device.value)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Here we replace the pipeline parts with versions converted to OpenVINO IR and compiled to specific device. Note that we use original pipeline tokenizer and scheduler." + ] + }, + { + "cell_type": "code", + "source": [ + "ov_pipe = OVTextToVideoSDPipeline(ov_vae_decoder, ov_text_encoder, tokenizer, ov_unet, scheduler)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Define a prompt\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "prompt = \"A panda eating bamboo on a rock.\"" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Let's generate a video for our prompt. For full list of arguments, see `__call__` function definition of `OVTextToVideoSDPipeline` class in [Build a pipeline](#Build-a-pipeline) section." + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Video generation\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "frames = ov_pipe(prompt, num_inference_steps=25)[\"frames\"]" + ] + }, + { + "cell_type": "code", + "source": [ + "images = [PIL.Image.fromarray(frame) for frame in frames]\n", + "images[0].save(\"output.gif\", save_all=True, append_images=images[1:], duration=125, loop=0)\n", + "with open(\"output.gif\", \"rb\") as gif_file:\n", + " b64 = f\"data:image/gif;base64,{base64.b64encode(gif_file.read()).decode()}\"\n", + "IPython.display.HTML(f'')" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Interactive demo\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def generate(prompt, seed, num_inference_steps, _=gr.Progress(track_tqdm=True)):\n", + " generator = torch.Generator().manual_seed(seed)\n", + " frames = ov_pipe(\n", + " prompt,\n", + " num_inference_steps=num_inference_steps,\n", + " generator=generator,\n", + " )[\"frames\"]\n", + " out_file = tempfile.NamedTemporaryFile(suffix=\".gif\", delete=False)\n", + " images = [PIL.Image.fromarray(frame) for frame in frames]\n", + " images[0].save(out_file, save_all=True, append_images=images[1:], duration=125, loop=0)\n", + " return out_file.name\n", + "\n", + "\n", + "demo = gr.Interface(\n", + " generate,\n", + " [\n", + " gr.Textbox(label=\"Prompt\"),\n", + " gr.Slider(0, 1000000, value=42, label=\"Seed\", step=1),\n", + " gr.Slider(10, 50, value=25, label=\"Number of inference steps\", step=1),\n", + " ],\n", + " gr.Image(label=\"Result\"),\n", + " examples=[\n", + " [\"An astronaut riding a horse.\", 0, 25],\n", + " [\"A panda eating bamboo on a rock.\", 0, 25],\n", + " [\"Spiderman is surfing.\", 0, 25],\n", + " ],\n", + " allow_flagging=\"never\",\n", + ")\n", + "\n", + "try:\n", + " demo.queue().launch(debug=True)\n", + "except Exception:\n", + " demo.queue().launch(share=True, debug=True)\n", + "# if you are launching remotely, specify server_name and server_port\n", + "# demo.launch(server_name='your server name', server_port='server port in int')\n", + "# Read more in the docs: https://gradio.app/docs/" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + { modified: 24, original: 24 }, + { modified: 25, original: 25 }, + { modified: 26, original: 26 }, + { modified: 27, original: 27 }, + { modified: 28, original: 28 }, + { modified: 29, original: 29 }, + { modified: 30, original: 30 }, + { modified: 31, original: 31 }, + { modified: 32, original: 32 }, + { modified: 33, original: 33 }, + { modified: 34, original: 34 }, + { modified: 35, original: 35 }, + { modified: 36, original: 36 }, + { modified: 37, original: 37 }, + { modified: 38, original: 38 }, + { modified: 39, original: 39 }, + { modified: 40, original: 40 }, + { modified: 41, original: 41 }, + { modified: 42, original: 42 }, + { modified: 43, original: 43 }, + { modified: 44, original: 44 }, + { modified: 45, original: 45 }, + { modified: 46, original: 46 }, + { modified: 47, original: 47 }, + { modified: 48, original: 48 }, + { modified: 49, original: 49 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 6, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 15, originalLength: 1, modifiedStart: 15, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 36, originalLength: 1, modifiedStart: 36, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 50, originalLength: 0, modifiedStart: 50, modifiedLength: 2 } satisfies IDiffChange, + ]); + + }); + test('Modification of multiple cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "id": "a2e7d62b-5779-4211-822c-457c77321f8b", + "source": [ + "# Convert and Optimize YOLOv11 instance segmentation model with OpenVINO™\n", + "\n", + "Instance segmentation goes a step further than object detection and involves identifying individual objects in an image and segmenting them from the rest of the image. Instance segmentation as an object detection are often used as key components in computer vision systems. \n", + "Applications that use real-time instance segmentation models include video analytics, robotics, autonomous vehicles, multi-object tracking and object counting, medical image analysis, and many others.\n", + "\n", + "\n", + "This tutorial demonstrates step-by-step instructions on how to run and optimize PyTorch YOLOv11 with OpenVINO. We consider the steps required for instance segmentation scenario. You can find more details about model on [model page](https://docs.ultralytics.com/models/yolo11/) in Ultralytics documentation.\n", + "\n", + "The tutorial consists of the following steps:\n", + "- Prepare the PyTorch model.\n", + "- Download and prepare a dataset.\n", + "- Validate the original model.\n", + "- Convert the PyTorch model to OpenVINO IR.\n", + "- Validate the converted model.\n", + "- Prepare and run optimization pipeline.\n", + "- Compare performance of the FP32 and quantized models.\n", + "- Compare accuracy of the FP32 and quantized models.\n", + "- Live demo\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Get PyTorch model](#Get-PyTorch-model)\n", + " - [Prerequisites](#Prerequisites)\n", + "- [Instantiate model](#Instantiate-model)\n", + " - [Convert model to OpenVINO IR](#Convert-model-to-OpenVINO-IR)\n", + " - [Verify model inference](#Verify-model-inference)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Test on single image](#Test-on-single-image)\n", + "- [Optimize model using NNCF Post-training Quantization API](#Optimize-model-using-NNCF-Post-training-Quantization-API)\n", + " - [Validate Quantized model inference](#Validate-Quantized-model-inference)\n", + "- [Compare the Original and Quantized Models](#Compare-the-Original-and-Quantized-Models)\n", + " - [Compare performance of the Original and Quantized Models](#Compare-performance-of-the-Original-and-Quantized-Models)\n", + "- [Other ways to optimize model](#Other-ways-to-optimize-model)\n", + "- [Live demo](#Live-demo)\n", + " - [Run Live Object Detection and Segmentation](#Run-Live-Object-Detection-and-Segmentation)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "d7a12678-b12f-48d1-9735-398855733e46", + "source": [ + "## Get PyTorch model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Generally, PyTorch models represent an instance of the [`torch.nn.Module`](https://pytorch.org/docs/stable/generated/torch.nn.Module.html) class, initialized by a state dictionary with model weights.\n", + "We will use the YOLOv11 nano model (also known as `yolo11n-seg`) pre-trained on a COCO dataset, which is available in this [repo](https://github.com/ultralytics/ultralytics). Similar steps are also applicable to other YOLOv11 models.\n", + "Typical steps to obtain a pre-trained model:\n", + "1. Create an instance of a model class.\n", + "2. Load a checkpoint state dict, which contains the pre-trained model weights.\n", + "3. Turn the model to evaluation for switching some operations to inference mode.\n", + "\n", + "In this case, the creators of the model provide an API that enables converting the YOLOv11 model to OpenVINO IR. Therefore, we do not need to do these steps manually." + ] + }, + { + "cell_type": "markdown", + "id": "e2267760-cbfe-41c6-958d-cad9f845d5bb", + "source": [ + "#### Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Install necessary packages." + ] + }, + { + "cell_type": "code", + "id": "30d04872-6916-454c-9211-6c644b50dc04", + "source": [ + "%pip install -q \"openvino>=2024.0.0\" \"nncf>=2.9.0\"\n", + "%pip install -q \"torch>=2.1\" \"torchvision>=0.16\" \"ultralytics==8.3.0\" opencv-python tqdm --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "id": "1bbe319c", + "source": [ + "Import required utility functions.\n", + "The lower cell will download the `notebook_utils` Python module from GitHub." + ] + }, + { + "cell_type": "code", + "id": "a2f6cd89", + "source": [ + "from pathlib import Path\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "from notebook_utils import download_file, VideoPlayer, device_widget" + ] + }, + { + "cell_type": "code", + "id": "373658bd-7e64-4479-914e-f2742d330afd", + "source": [ + "# Download a test sample\n", + "IMAGE_PATH = Path(\"./data/coco_bike.jpg\")\n", + "download_file(\n", + " url=\"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/image/coco_bike.jpg\",\n", + " filename=IMAGE_PATH.name,\n", + " directory=IMAGE_PATH.parent,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ee32fd08-650c-4751-bb41-d8afccb2495e", + "source": [ + "## Instantiate model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "For loading the model, required to specify a path to the model checkpoint. It can be some local path or name available on models hub (in this case model checkpoint will be downloaded automatically). \n", + "You can select model using widget bellow:" + ] + }, + { + "cell_type": "code", + "id": "7c3963a5-b50b-4ffe-b710-ccd1f4a65380", + "source": [ + "import ipywidgets as widgets\n", + "\n", + "model_id = [\n", + " \"yolo11n-seg\",\n", + " \"yolo11s-seg\",\n", + " \"yolo11m-seg\",\n", + " \"yolo11l-seg\",\n", + " \"yolo11x-seg\",\n", + " \"yolov8n-seg\",\n", + " \"yolov8s-seg\",\n", + " \"yolov8m-seg\",\n", + " \"yolov8l-seg\",\n", + " \"yolov8x-seg\",\n", + "]\n", + "\n", + "model_name = widgets.Dropdown(options=model_id, value=model_id[0], description=\"Model\")\n", + "\n", + "model_name" + ] + }, + { + "cell_type": "markdown", + "id": "60105d86-d7e0-47a9-ad88-4fff014ba3d8", + "source": [ + "Making prediction, the model accepts a path to input image and returns list with Results class object. Results contains boxes for object detection model and boxes and masks for segmentation model. Also it contains utilities for processing results, for example, `plot()` method for drawing.\n", + "\n", + "Let us consider the examples:" + ] + }, + { + "cell_type": "code", + "id": "d4994e1e-a3ee-4620-bc8b-237a55c47742", + "source": [ + "from PIL import Image\n", + "from ultralytics import YOLO\n", + "\n", + "SEG_MODEL_NAME = model_name.value\n", + "\n", + "seg_model = YOLO(f\"{SEG_MODEL_NAME}.pt\")\n", + "label_map = seg_model.model.names\n", + "\n", + "res = seg_model(IMAGE_PATH)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "e345ffcc-c4b8-44ba-8b03-f37e63a060da", + "source": [ + "### Convert model to OpenVINO IR\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Ultralytics provides API for convenient model exporting to different formats including OpenVINO IR. `model.export` is responsible for model conversion. We need to specify the format, and additionally, we can preserve dynamic shapes in the model." + ] + }, + { + "cell_type": "code", + "id": "5c3ef363-f6a1-4fe5-b7ff-eb5a9c0789f1", + "source": [ + "# instance segmentation model\n", + "seg_model_path = Path(f\"{SEG_MODEL_NAME}_openvino_model/{SEG_MODEL_NAME}.xml\")\n", + "if not seg_model_path.exists():\n", + " seg_model.export(format=\"openvino\", dynamic=True, half=True)" + ] + }, + { + "cell_type": "markdown", + "id": "713cd45f-2b19-4a1e-bc9d-5e69bd95d896", + "source": [ + "### Verify model inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We can reuse the base model pipeline for pre- and postprocessing just replacing the inference method where we will use the IR model for inference." + ] + }, + { + "cell_type": "markdown", + "id": "f9b9c472", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "id": "4e0652b2", + "source": [ + "device = device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "id": "cd163eab-e803-4ae8-96da-22c3bf303630", + "source": [ + "### Test on single image\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "34a65afa-4201-428b-af11-b35e0d206e93", + "source": [ + "import openvino as ov\n", + "\n", + "core = ov.Core()\n", + "seg_ov_model = core.read_model(seg_model_path)\n", + "\n", + "ov_config = {}\n", + "if device.value != \"CPU\":\n", + " seg_ov_model.reshape({0: [1, 3, 640, 640]})\n", + "if \"GPU\" in device.value or (\"AUTO\" in device.value and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "seg_compiled_model = core.compile_model(seg_ov_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "id": "389a4698-b7fc-4aa2-9833-9681f575d88b", + "source": [ + "seg_model = YOLO(seg_model_path.parent, task=\"segment\")\n", + "\n", + "if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + "seg_model.predictor.model.ov_compiled_model = seg_compiled_model" + ] + }, + { + "cell_type": "code", + "id": "12d2522f-e939-4512-b74a-3e20baf1edc8", + "source": [ + "res = seg_model(IMAGE_PATH)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "7e918cf6-cdce-4e90-a6d7-c435a3c08d93", + "source": [ + "Great! The result is the same, as produced by original models." + ] + }, + { + "cell_type": "markdown", + "id": "13b69e12-2e22-44c1-bfed-fdc88f6c424d", + "source": [ + "## Optimize model using NNCF Post-training Quantization API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "[NNCF](https://github.com/openvinotoolkit/nncf) provides a suite of advanced algorithms for Neural Networks inference optimization in OpenVINO with minimal accuracy drop.\n", + "We will use 8-bit quantization in post-training mode (without the fine-tuning pipeline) to optimize YOLOv11.\n", + "\n", + "The optimization process contains the following steps:\n", + "\n", + "1. Create a Dataset for quantization.\n", + "2. Run `nncf.quantize` for getting an optimized model.\n", + "3. Serialize OpenVINO IR model, using the `openvino.runtime.serialize` function." + ] + }, + { + "cell_type": "markdown", + "id": "7d7eeae8-f3f4-4e95-b1a6-a23085f03ebc", + "source": [ + "Please select below whether you would like to run quantization to improve model inference speed." + ] + }, + { + "cell_type": "code", + "id": "4690b9fc-c49f-4882-b8a5-c82f842a9126", + "source": [ + "import ipywidgets as widgets\n", + "\n", + "int8_model_seg_path = Path(f\"{SEG_MODEL_NAME}_openvino_int8_model/{SEG_MODEL_NAME}.xml\")\n", + "quantized_seg_model = None\n", + "\n", + "to_quantize = widgets.Checkbox(\n", + " value=True,\n", + " description=\"Quantization\",\n", + " disabled=False,\n", + ")\n", + "\n", + "to_quantize" + ] + }, + { + "cell_type": "markdown", + "id": "73cfdbd4-3b65-49e0-b8ab-ceb1d0362481", + "source": [ + "Let's load `skip magic` extension to skip quantization if `to_quantize` is not selected" + ] + }, + { + "cell_type": "code", + "id": "bc6121aa-3a07-4066-a123-fe0ed0e72478", + "source": [ + "# Fetch skip_kernel_extension module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/skip_kernel_extension.py\",\n", + ")\n", + "open(\"skip_kernel_extension.py\", \"w\").write(r.text)\n", + "\n", + "%load_ext skip_kernel_extension" + ] + }, + { + "cell_type": "markdown", + "id": "94fb2515-3645-4798-bc7c-082129ac86c0", + "source": [ + "Reuse validation dataloader in accuracy testing for quantization. \n", + "For that, it should be wrapped into the `nncf.Dataset` object and define a transformation function for getting only input tensors." + ] + }, + { + "cell_type": "code", + "id": "fd994958-6988-4a1d-ac7f-3efbd97135cc", + "source": [ + "# %%skip not $to_quantize.value\n", + "\n", + "\n", + "import nncf\n", + "from typing import Dict\n", + "\n", + "from zipfile import ZipFile\n", + "\n", + "from ultralytics.data.utils import DATASETS_DIR\n", + "from ultralytics.utils import DEFAULT_CFG\n", + "from ultralytics.cfg import get_cfg\n", + "from ultralytics.data.converter import coco80_to_coco91_class\n", + "from ultralytics.data.utils import check_det_dataset\n", + "from ultralytics.utils import ops\n", + "\n", + "\n", + "if not int8_model_seg_path.exists():\n", + " DATA_URL = \"http://images.cocodataset.org/zips/val2017.zip\"\n", + " LABELS_URL = \"https://github.com/ultralytics/yolov5/releases/download/v1.0/coco2017labels-segments.zip\"\n", + " CFG_URL = \"https://raw.githubusercontent.com/ultralytics/ultralytics/v8.1.0/ultralytics/cfg/datasets/coco.yaml\"\n", + "\n", + " OUT_DIR = DATASETS_DIR\n", + "\n", + " DATA_PATH = OUT_DIR / \"val2017.zip\"\n", + " LABELS_PATH = OUT_DIR / \"coco2017labels-segments.zip\"\n", + " CFG_PATH = OUT_DIR / \"coco.yaml\"\n", + "\n", + " download_file(DATA_URL, DATA_PATH.name, DATA_PATH.parent)\n", + " download_file(LABELS_URL, LABELS_PATH.name, LABELS_PATH.parent)\n", + " download_file(CFG_URL, CFG_PATH.name, CFG_PATH.parent)\n", + "\n", + " if not (OUT_DIR / \"coco/labels\").exists():\n", + " with ZipFile(LABELS_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR)\n", + " with ZipFile(DATA_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR / \"coco/images\")\n", + "\n", + " args = get_cfg(cfg=DEFAULT_CFG)\n", + " args.data = str(CFG_PATH)\n", + " seg_validator = seg_model.task_map[seg_model.task][\"validator\"](args=args)\n", + " seg_validator.data = check_det_dataset(args.data)\n", + " seg_validator.stride = 32\n", + " seg_data_loader = seg_validator.get_dataloader(OUT_DIR / \"coco/\", 1)\n", + "\n", + " seg_validator.is_coco = True\n", + " seg_validator.class_map = coco80_to_coco91_class()\n", + " seg_validator.names = label_map\n", + " seg_validator.metrics.names = seg_validator.names\n", + " seg_validator.nc = 80\n", + " seg_validator.nm = 32\n", + " seg_validator.process = ops.process_mask\n", + " seg_validator.plot_masks = []\n", + "\n", + " def transform_fn(data_item: Dict):\n", + " \"\"\"\n", + " Quantization transform function. Extracts and preprocess input data from dataloader item for quantization.\n", + " Parameters:\n", + " data_item: Dict with data item produced by DataLoader during iteration\n", + " Returns:\n", + " input_tensor: Input data for quantization\n", + " \"\"\"\n", + " input_tensor = seg_validator.preprocess(data_item)[\"img\"].numpy()\n", + " return input_tensor\n", + "\n", + " quantization_dataset = nncf.Dataset(seg_data_loader, transform_fn)" + ] + }, + { + "cell_type": "markdown", + "id": "91629284-e261-494d-9464-146ed7190084", + "source": [ + "The `nncf.quantize` function provides an interface for model quantization. It requires an instance of the OpenVINO Model and quantization dataset. \n", + "Optionally, some additional parameters for the configuration quantization process (number of samples for quantization, preset, ignored scope, etc.) can be provided. Ultralytics models contain non-ReLU activation functions, which require asymmetric quantization of activations. To achieve a better result, we will use a `mixed` quantization preset. It provides symmetric quantization of weights and asymmetric quantization of activations. For more accurate results, we should keep the operation in the postprocessing subgraph in floating point precision, using the `ignored_scope` parameter.\n", + "\n", + ">**Note**: Model post-training quantization is time-consuming process. Be patient, it can take several minutes depending on your hardware." + ] + }, + { + "cell_type": "code", + "id": "b2e8cec4-d0b3-4da0-b54c-7964e7bcbfe2", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "if not int8_model_seg_path.exists():\n", + " ignored_scope = nncf.IgnoredScope( # post-processing\n", + " subgraphs=[\n", + " nncf.Subgraph(inputs=[f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_1\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_2\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_7\"],\n", + " outputs=[f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_8\"])\n", + " ]\n", + " )\n", + "\n", + " # Segmentation model\n", + " quantized_seg_model = nncf.quantize(\n", + " seg_ov_model,\n", + " quantization_dataset,\n", + " preset=nncf.QuantizationPreset.MIXED,\n", + " ignored_scope=ignored_scope\n", + " )\n", + "\n", + " print(f\"Quantized segmentation model will be saved to {int8_model_seg_path}\")\n", + " ov.save_model(quantized_seg_model, str(int8_model_seg_path))" + ] + }, + { + "cell_type": "markdown", + "id": "c8c92a68-3eb8-4ea5-9e35-d496f45b3cc3", + "source": [ + "### Validate Quantized model inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "`nncf.quantize` returns the OpenVINO Model class instance, which is suitable for loading on a device for making predictions. `INT8` model input data and output result formats have no difference from the floating point model representation. Therefore, we can reuse the same `detect` function defined above for getting the `INT8` model result on the image." + ] + }, + { + "cell_type": "code", + "id": "4003a3ae", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "711200ec-2f26-47cc-b62d-3802961d5711", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "if quantized_seg_model is None:\n", + " quantized_seg_model = core.read_model(int8_model_seg_path)\n", + "\n", + "ov_config = {}\n", + "if device.value != \"CPU\":\n", + " quantized_seg_model.reshape({0: [1, 3, 640, 640]})\n", + "if \"GPU\" in device.value or (\"AUTO\" in device.value and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "\n", + "quantized_seg_compiled_model = core.compile_model(quantized_seg_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "id": "b2e3ea3b-5935-4474-9f08-f51fc688b9c0", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "\n", + "if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + "seg_model.predictor.model.ov_compiled_model = quantized_seg_compiled_model" + ] + }, + { + "cell_type": "code", + "id": "bef2ed5d-5762-41a6-af2e-ee4f9fd56557", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "res = seg_model(IMAGE_PATH)\n", + "display(Image.fromarray(res[0].plot()[:, :, ::-1]))" + ] + }, + { + "cell_type": "markdown", + "id": "b289f057", + "source": [ + "## Compare the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "id": "59081cae-9c92-46f0-8117-ed8c6fa6ff19", + "source": [ + "### Compare performance of the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "Finally, use the OpenVINO [Benchmark Tool](https://docs.openvino.ai/2024/learn-openvino/openvino-samples/benchmark-tool.html) to measure the inference performance of the `FP32` and `INT8` models.\n", + "\n", + "> **Note**: For more accurate performance, it is recommended to run `benchmark_app` in a terminal/command prompt after closing other applications. Run `benchmark_app -m -d CPU -shape \"\"` to benchmark async inference on CPU on specific input data shape for one minute. Change `CPU` to `GPU` to benchmark on GPU. Run `benchmark_app --help` to see an overview of all command-line options." + ] + }, + { + "cell_type": "code", + "id": "b8677df4", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "546828d2", + "source": [ + "if int8_model_seg_path.exists():\n", + " !benchmark_app -m $seg_model_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "code", + "id": "1d43554b", + "source": [ + "if int8_model_seg_path.exists():\n", + " !benchmark_app -m $int8_model_seg_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "markdown", + "id": "184bcebd-588d-4b4d-a60a-0fa753a07ef9", + "source": [ + "## Other ways to optimize model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The performance could be also improved by another OpenVINO method such as async inference pipeline or preprocessing API.\n", + "\n", + "Async Inference pipeline help to utilize the device more optimal. The key advantage of the Async API is that when a device is busy with inference, the application can perform other tasks in parallel (for example, populating inputs or scheduling other requests) rather than wait for the current inference to complete first. To understand how to perform async inference using openvino, refer to [Async API tutorial](../async-api/async-api.ipynb)\n", + "\n", + "Preprocessing API enables making preprocessing a part of the model reducing application code and dependency on additional image processing libraries. \n", + "The main advantage of Preprocessing API is that preprocessing steps will be integrated into the execution graph and will be performed on a selected device (CPU/GPU etc.) rather than always being executed on CPU as part of an application. This will also improve selected device utilization. For more information, refer to the overview of [Preprocessing API tutorial](../optimize-preprocessing/optimize-preprocessing.ipynb). To see, how it could be used with YOLOV8 object detection model, please, see [Convert and Optimize YOLOv8 real-time object detection with OpenVINO tutorial](./yolov8-object-detection.ipynb)" + ] + }, + { + "cell_type": "markdown", + "id": "2e3b4862-c182-4ce4-a473-9f38d98deab8", + "source": [ + "## Live demo\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The following code runs model inference on a video:" + ] + }, + { + "cell_type": "code", + "id": "4c7bb92e-e301-45a9-b5ff-f7953fad298f", + "source": [ + "import collections\n", + "import time\n", + "import cv2\n", + "from IPython import display\n", + "import numpy as np\n", + "\n", + "\n", + "def run_instance_segmentation(\n", + " source=0,\n", + " flip=False,\n", + " use_popup=False,\n", + " skip_first_frames=0,\n", + " model=seg_model,\n", + " device=device.value,\n", + "):\n", + " player = None\n", + "\n", + " ov_config = {}\n", + " if device != \"CPU\":\n", + " model.reshape({0: [1, 3, 640, 640]})\n", + " if \"GPU\" in device or (\"AUTO\" in device and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + " compiled_model = core.compile_model(model, device, ov_config)\n", + "\n", + " if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + " seg_model.predictor.model.ov_compiled_model = compiled_model\n", + "\n", + " try:\n", + " # Create a video player to play with target fps.\n", + " player = VideoPlayer(source=source, flip=flip, fps=30, skip_first_frames=skip_first_frames)\n", + " # Start capturing.\n", + " player.start()\n", + " if use_popup:\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(winname=title, flags=cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_AUTOSIZE)\n", + "\n", + " processing_times = collections.deque()\n", + " while True:\n", + " # Grab the frame.\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + " # If the frame is larger than full HD, reduce size to improve the performance.\n", + " scale = 1280 / max(frame.shape)\n", + " if scale < 1:\n", + " frame = cv2.resize(\n", + " src=frame,\n", + " dsize=None,\n", + " fx=scale,\n", + " fy=scale,\n", + " interpolation=cv2.INTER_AREA,\n", + " )\n", + " # Get the results.\n", + " input_image = np.array(frame)\n", + "\n", + " start_time = time.time()\n", + " detections = seg_model(input_image)\n", + " stop_time = time.time()\n", + " frame = detections[0].plot()\n", + "\n", + " processing_times.append(stop_time - start_time)\n", + " # Use processing times from last 200 frames.\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " _, f_width = frame.shape[:2]\n", + " # Mean processing time [ms].\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + " cv2.putText(\n", + " img=frame,\n", + " text=f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " org=(20, 40),\n", + " fontFace=cv2.FONT_HERSHEY_COMPLEX,\n", + " fontScale=f_width / 1000,\n", + " color=(0, 0, 255),\n", + " thickness=1,\n", + " lineType=cv2.LINE_AA,\n", + " )\n", + " # Use this workaround if there is flickering.\n", + " if use_popup:\n", + " cv2.imshow(winname=title, mat=frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # Encode numpy array to jpg.\n", + " _, encoded_img = cv2.imencode(ext=\".jpg\", img=frame, params=[cv2.IMWRITE_JPEG_QUALITY, 100])\n", + " # Create an IPython image.\n", + " i = display.Image(data=encoded_img)\n", + " # Display the image in this notebook.\n", + " display.clear_output(wait=True)\n", + " display.display(i)\n", + " # ctrl-c\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " # any different error\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " if player is not None:\n", + " # Stop capturing.\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()" + ] + }, + { + "cell_type": "markdown", + "id": "cc5b4ba6-f478-4417-b09d-93fee5adca41", + "source": [ + "### Run Live Object Detection and Segmentation\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Use a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + ">**NOTE**: To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a remote server (for example, in Binder or Google Colab service), the webcam will not work. By default, the lower cell will run model inference on a video file. If you want to try live inference on your webcam set `WEBCAM_INFERENCE = True`" + ] + }, + { + "cell_type": "code", + "id": "90708017", + "source": [ + "WEBCAM_INFERENCE = False\n", + "\n", + "if WEBCAM_INFERENCE:\n", + " VIDEO_SOURCE = 0 # Webcam\n", + "else:\n", + " VIDEO_SOURCE = \"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/video/people.mp4\"" + ] + }, + { + "cell_type": "code", + "id": "e8f1239c", + "source": [ + "device" + ] + }, + { + "cell_type": "code", + "id": "440f2d0e", + "source": [ + "run_instance_segmentation(\n", + " source=VIDEO_SOURCE,\n", + " flip=True,\n", + " use_popup=False,\n", + " model=seg_ov_model,\n", + " device=device.value,\n", + ")" + ] + } + ] + .map(fromJupyterCell) + , + [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "a2e7d62b-5779-4211-822c-457c77321f8b", + "metadata": {}, + "source": [ + "# Convert and Optimize YOLOv11 instance segmentation model with OpenVINO™\n", + "\n", + "Instance segmentation goes a step further than object detection and involves identifying individual objects in an image and segmenting them from the rest of the image. Instance segmentation as an object detection are often used as key components in computer vision systems. \n", + "Applications that use real-time instance segmentation models include video analytics, robotics, autonomous vehicles, multi-object tracking and object counting, medical image analysis, and many others.\n", + "\n", + "\n", + "This tutorial demonstrates step-by-step instructions on how to run and optimize PyTorch YOLOv11 with OpenVINO. We consider the steps required for instance segmentation scenario. You can find more details about model on [model page](https://docs.ultralytics.com/models/yolo11/) in Ultralytics documentation.\n", + "\n", + "The tutorial consists of the following steps:\n", + "- Prepare the PyTorch model.\n", + "- Download and prepare a dataset.\n", + "- Validate the original model.\n", + "- Convert the PyTorch model to OpenVINO IR.\n", + "- Validate the converted model.\n", + "- Prepare and run optimization pipeline.\n", + "- Compare performance of the FP32 and quantized models.\n", + "- Compare accuracy of the FP32 and quantized models.\n", + "- Live demo\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Get PyTorch model](#Get-PyTorch-model)\n", + " - [Prerequisites](#Prerequisites)\n", + "- [Instantiate model](#Instantiate-model)\n", + " - [Convert model to OpenVINO IR](#Convert-model-to-OpenVINO-IR)\n", + " - [Verify model inference](#Verify-model-inference)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Test on single image](#Test-on-single-image)\n", + "- [Optimize model using NNCF Post-training Quantization API](#Optimize-model-using-NNCF-Post-training-Quantization-API)\n", + " - [Validate Quantized model inference](#Validate-Quantized-model-inference)\n", + "- [Compare the Original and Quantized Models](#Compare-the-Original-and-Quantized-Models)\n", + " - [Compare performance of the Original and Quantized Models](#Compare-performance-of-the-Original-and-Quantized-Models)\n", + "- [Other ways to optimize model](#Other-ways-to-optimize-model)\n", + "- [Live demo](#Live-demo)\n", + " - [Run Live Object Detection and Segmentation](#Run-Live-Object-Detection-and-Segmentation)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d7a12678-b12f-48d1-9735-398855733e46", + "metadata": {}, + "source": [ + "## Get PyTorch model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Generally, PyTorch models represent an instance of the [`torch.nn.Module`](https://pytorch.org/docs/stable/generated/torch.nn.Module.html) class, initialized by a state dictionary with model weights.\n", + "We will use the YOLOv11 nano model (also known as `yolo11n-seg`) pre-trained on a COCO dataset, which is available in this [repo](https://github.com/ultralytics/ultralytics). Similar steps are also applicable to other YOLOv11 models.\n", + "Typical steps to obtain a pre-trained model:\n", + "1. Create an instance of a model class.\n", + "2. Load a checkpoint state dict, which contains the pre-trained model weights.\n", + "3. Turn the model to evaluation for switching some operations to inference mode.\n", + "\n", + "In this case, the creators of the model provide an API that enables converting the YOLOv11 model to OpenVINO IR. Therefore, we do not need to do these steps manually." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e2267760-cbfe-41c6-958d-cad9f845d5bb", + "metadata": {}, + "source": [ + "#### Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Install necessary packages." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30d04872-6916-454c-9211-6c644b50dc04", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install -q \"openvino>=2024.0.0\" \"nncf>=2.9.0\"\n", + "%pip install -q \"torch>=2.1\" \"torchvision>=0.16\" \"ultralytics==8.3.0\" opencv-python tqdm --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1bbe319c", + "metadata": {}, + "source": [ + "Import required utility functions.\n", + "The lower cell will download the `notebook_utils` Python module from GitHub." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2f6cd89", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import requests\n", + "\n", + "if not Path(\"notebook_utils.py\").exists():\n", + " r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + " )\n", + "\n", + " open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "\n", + "from notebook_utils import download_file, VideoPlayer, device_widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "373658bd-7e64-4479-914e-f2742d330afd", + "metadata": {}, + "outputs": [], + "source": [ + "# Download a test sample\n", + "IMAGE_PATH = Path(\"./data/coco_bike.jpg\")\n", + "\n", + "if not IMAGE_PATH.exists():\n", + " download_file(\n", + " url=\"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/image/coco_bike.jpg\",\n", + " filename=IMAGE_PATH.name,\n", + " directory=IMAGE_PATH.parent,\n", + " )" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ee32fd08-650c-4751-bb41-d8afccb2495e", + "metadata": {}, + "source": [ + "## Instantiate model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "For loading the model, required to specify a path to the model checkpoint. It can be some local path or name available on models hub (in this case model checkpoint will be downloaded automatically). \n", + "You can select model using widget bellow:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c3963a5-b50b-4ffe-b710-ccd1f4a65380", + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "\n", + "model_id = [\n", + " \"yolo11n-seg\",\n", + " \"yolo11s-seg\",\n", + " \"yolo11m-seg\",\n", + " \"yolo11l-seg\",\n", + " \"yolo11x-seg\",\n", + " \"yolov8n-seg\",\n", + " \"yolov8s-seg\",\n", + " \"yolov8m-seg\",\n", + " \"yolov8l-seg\",\n", + " \"yolov8x-seg\",\n", + "]\n", + "\n", + "model_name = widgets.Dropdown(options=model_id, value=model_id[0], description=\"Model\")\n", + "\n", + "model_name" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "60105d86-d7e0-47a9-ad88-4fff014ba3d8", + "metadata": {}, + "source": [ + "Making prediction, the model accepts a path to input image and returns list with Results class object. Results contains boxes for object detection model and boxes and masks for segmentation model. Also it contains utilities for processing results, for example, `plot()` method for drawing.\n", + "\n", + "Let us consider the examples:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4994e1e-a3ee-4620-bc8b-237a55c47742", + "metadata": {}, + "outputs": [], + "source": [ + "from PIL import Image\n", + "from ultralytics import YOLO\n", + "\n", + "SEG_MODEL_NAME = model_name.value\n", + "\n", + "seg_model = YOLO(f\"{SEG_MODEL_NAME}.pt\")\n", + "label_map = seg_model.model.names\n", + "\n", + "res = seg_model(IMAGE_PATH)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e345ffcc-c4b8-44ba-8b03-f37e63a060da", + "metadata": {}, + "source": [ + "### Convert model to OpenVINO IR\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Ultralytics provides API for convenient model exporting to different formats including OpenVINO IR. `model.export` is responsible for model conversion. We need to specify the format, and additionally, we can preserve dynamic shapes in the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c3ef363-f6a1-4fe5-b7ff-eb5a9c0789f1", + "metadata": {}, + "outputs": [], + "source": [ + "# instance segmentation model\n", + "seg_model_path = Path(f\"{SEG_MODEL_NAME}_openvino_model/{SEG_MODEL_NAME}.xml\")\n", + "if not seg_model_path.exists():\n", + " seg_model.export(format=\"openvino\", dynamic=True, half=True)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "713cd45f-2b19-4a1e-bc9d-5e69bd95d896", + "metadata": {}, + "source": [ + "### Verify model inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We can reuse the base model pipeline for pre- and postprocessing just replacing the inference method where we will use the IR model for inference." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f9b9c472", + "metadata": {}, + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e0652b2", + "metadata": {}, + "outputs": [], + "source": [ + "device = device_widget()\n", + "\n", + "device" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "cd163eab-e803-4ae8-96da-22c3bf303630", + "metadata": {}, + "source": [ + "### Test on single image\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34a65afa-4201-428b-af11-b35e0d206e93", + "metadata": {}, + "outputs": [], + "source": [ + "import openvino as ov\n", + "\n", + "core = ov.Core()\n", + "seg_ov_model = core.read_model(seg_model_path)\n", + "\n", + "ov_config = {}\n", + "if device.value != \"CPU\":\n", + " seg_ov_model.reshape({0: [1, 3, 640, 640]})\n", + "if \"GPU\" in device.value or (\"AUTO\" in device.value and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "seg_compiled_model = core.compile_model(seg_ov_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "389a4698-b7fc-4aa2-9833-9681f575d88b", + "metadata": {}, + "outputs": [], + "source": [ + "seg_model = YOLO(seg_model_path.parent, task=\"segment\")\n", + "\n", + "if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + "seg_model.predictor.model.ov_compiled_model = seg_compiled_model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12d2522f-e939-4512-b74a-3e20baf1edc8", + "metadata": {}, + "outputs": [], + "source": [ + "res = seg_model(IMAGE_PATH)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7e918cf6-cdce-4e90-a6d7-c435a3c08d93", + "metadata": {}, + "source": [ + "Great! The result is the same, as produced by original models." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "13b69e12-2e22-44c1-bfed-fdc88f6c424d", + "metadata": {}, + "source": [ + "## Optimize model using NNCF Post-training Quantization API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "[NNCF](https://github.com/openvinotoolkit/nncf) provides a suite of advanced algorithms for Neural Networks inference optimization in OpenVINO with minimal accuracy drop.\n", + "We will use 8-bit quantization in post-training mode (without the fine-tuning pipeline) to optimize YOLOv11.\n", + "\n", + "The optimization process contains the following steps:\n", + "\n", + "1. Create a Dataset for quantization.\n", + "2. Run `nncf.quantize` for getting an optimized model.\n", + "3. Serialize OpenVINO IR model, using the `openvino.runtime.serialize` function." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7d7eeae8-f3f4-4e95-b1a6-a23085f03ebc", + "metadata": {}, + "source": [ + "Please select below whether you would like to run quantization to improve model inference speed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4690b9fc-c49f-4882-b8a5-c82f842a9126", + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "\n", + "int8_model_seg_path = Path(f\"{SEG_MODEL_NAME}_openvino_int8_model/{SEG_MODEL_NAME}.xml\")\n", + "quantized_seg_model = None\n", + "\n", + "to_quantize = widgets.Checkbox(\n", + " value=True,\n", + " description=\"Quantization\",\n", + " disabled=False,\n", + ")\n", + "\n", + "to_quantize" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "73cfdbd4-3b65-49e0-b8ab-ceb1d0362481", + "metadata": {}, + "source": [ + "Let's load `skip magic` extension to skip quantization if `to_quantize` is not selected" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc6121aa-3a07-4066-a123-fe0ed0e72478", + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch skip_kernel_extension module\n", + "import requests\n", + "\n", + "\n", + "if not Path(\"skip_kernel_extension.py\").exists():\n", + " r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/skip_kernel_extension.py\",\n", + " )\n", + " open(\"skip_kernel_extension.py\", \"w\").write(r.text)\n", + "\n", + "%load_ext skip_kernel_extension" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "94fb2515-3645-4798-bc7c-082129ac86c0", + "metadata": {}, + "source": [ + "Reuse validation dataloader in accuracy testing for quantization. \n", + "For that, it should be wrapped into the `nncf.Dataset` object and define a transformation function for getting only input tensors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd994958-6988-4a1d-ac7f-3efbd97135cc", + "metadata": {}, + "outputs": [], + "source": [ + "# %%skip not $to_quantize.value\n", + "\n", + "\n", + "import nncf\n", + "from typing import Dict\n", + "\n", + "from zipfile import ZipFile\n", + "\n", + "from ultralytics.data.utils import DATASETS_DIR\n", + "from ultralytics.utils import DEFAULT_CFG\n", + "from ultralytics.cfg import get_cfg\n", + "from ultralytics.data.converter import coco80_to_coco91_class\n", + "from ultralytics.data.utils import check_det_dataset\n", + "from ultralytics.utils import ops\n", + "\n", + "\n", + "if not int8_model_seg_path.exists():\n", + " DATA_URL = \"http://images.cocodataset.org/zips/val2017.zip\"\n", + " LABELS_URL = \"https://github.com/ultralytics/yolov5/releases/download/v1.0/coco2017labels-segments.zip\"\n", + " CFG_URL = \"https://raw.githubusercontent.com/ultralytics/ultralytics/v8.1.0/ultralytics/cfg/datasets/coco.yaml\"\n", + "\n", + " OUT_DIR = DATASETS_DIR\n", + "\n", + " DATA_PATH = OUT_DIR / \"val2017.zip\"\n", + " LABELS_PATH = OUT_DIR / \"coco2017labels-segments.zip\"\n", + " CFG_PATH = OUT_DIR / \"coco.yaml\"\n", + "\n", + " download_file(DATA_URL, DATA_PATH.name, DATA_PATH.parent)\n", + " download_file(LABELS_URL, LABELS_PATH.name, LABELS_PATH.parent)\n", + " download_file(CFG_URL, CFG_PATH.name, CFG_PATH.parent)\n", + "\n", + " if not (OUT_DIR / \"coco/labels\").exists():\n", + " with ZipFile(LABELS_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR)\n", + " with ZipFile(DATA_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR / \"coco/images\")\n", + "\n", + " args = get_cfg(cfg=DEFAULT_CFG)\n", + " args.data = str(CFG_PATH)\n", + " seg_validator = seg_model.task_map[seg_model.task][\"validator\"](args=args)\n", + " seg_validator.data = check_det_dataset(args.data)\n", + " seg_validator.stride = 32\n", + " seg_data_loader = seg_validator.get_dataloader(OUT_DIR / \"coco/\", 1)\n", + "\n", + " seg_validator.is_coco = True\n", + " seg_validator.class_map = coco80_to_coco91_class()\n", + " seg_validator.names = label_map\n", + " seg_validator.metrics.names = seg_validator.names\n", + " seg_validator.nc = 80\n", + " seg_validator.nm = 32\n", + " seg_validator.process = ops.process_mask\n", + " seg_validator.plot_masks = []\n", + "\n", + " def transform_fn(data_item: Dict):\n", + " \"\"\"\n", + " Quantization transform function. Extracts and preprocess input data from dataloader item for quantization.\n", + " Parameters:\n", + " data_item: Dict with data item produced by DataLoader during iteration\n", + " Returns:\n", + " input_tensor: Input data for quantization\n", + " \"\"\"\n", + " input_tensor = seg_validator.preprocess(data_item)[\"img\"].numpy()\n", + " return input_tensor\n", + "\n", + " quantization_dataset = nncf.Dataset(seg_data_loader, transform_fn)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "91629284-e261-494d-9464-146ed7190084", + "metadata": {}, + "source": [ + "The `nncf.quantize` function provides an interface for model quantization. It requires an instance of the OpenVINO Model and quantization dataset. \n", + "Optionally, some additional parameters for the configuration quantization process (number of samples for quantization, preset, ignored scope, etc.) can be provided. Ultralytics models contain non-ReLU activation functions, which require asymmetric quantization of activations. To achieve a better result, we will use a `mixed` quantization preset. It provides symmetric quantization of weights and asymmetric quantization of activations. For more accurate results, we should keep the operation in the postprocessing subgraph in floating point precision, using the `ignored_scope` parameter.\n", + "\n", + ">**Note**: Model post-training quantization is time-consuming process. Be patient, it can take several minutes depending on your hardware." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2e8cec4-d0b3-4da0-b54c-7964e7bcbfe2", + "metadata": { + "test_replace": { + "ignored_scope=ignored_scope\n": "ignored_scope=ignored_scope, subset_size=10\n" + } + }, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "if not int8_model_seg_path.exists():\n", + " ignored_scope = nncf.IgnoredScope( # post-processing\n", + " subgraphs=[\n", + " nncf.Subgraph(inputs=[f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_1\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_2\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_7\"],\n", + " outputs=[f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_8\"])\n", + " ]\n", + " )\n", + "\n", + " # Segmentation model\n", + " quantized_seg_model = nncf.quantize(\n", + " seg_ov_model,\n", + " quantization_dataset,\n", + " preset=nncf.QuantizationPreset.MIXED,\n", + " ignored_scope=ignored_scope\n", + " )\n", + "\n", + " print(f\"Quantized segmentation model will be saved to {int8_model_seg_path}\")\n", + " ov.save_model(quantized_seg_model, str(int8_model_seg_path))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c8c92a68-3eb8-4ea5-9e35-d496f45b3cc3", + "metadata": {}, + "source": [ + "### Validate Quantized model inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "`nncf.quantize` returns the OpenVINO Model class instance, which is suitable for loading on a device for making predictions. `INT8` model input data and output result formats have no difference from the floating point model representation. Therefore, we can reuse the same `detect` function defined above for getting the `INT8` model result on the image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4003a3ae", + "metadata": {}, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "711200ec-2f26-47cc-b62d-3802961d5711", + "metadata": {}, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "if quantized_seg_model is None:\n", + " quantized_seg_model = core.read_model(int8_model_seg_path)\n", + "\n", + "ov_config = {}\n", + "if device.value != \"CPU\":\n", + " quantized_seg_model.reshape({0: [1, 3, 640, 640]})\n", + "if \"GPU\" in device.value or (\"AUTO\" in device.value and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "\n", + "quantized_seg_compiled_model = core.compile_model(quantized_seg_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2e3ea3b-5935-4474-9f08-f51fc688b9c0", + "metadata": {}, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "\n", + "if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + "seg_model.predictor.model.ov_compiled_model = quantized_seg_compiled_model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bef2ed5d-5762-41a6-af2e-ee4f9fd56557", + "metadata": {}, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "res = seg_model(IMAGE_PATH)\n", + "display(Image.fromarray(res[0].plot()[:, :, ::-1]))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b289f057", + "metadata": {}, + "source": [ + "## Compare the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "59081cae-9c92-46f0-8117-ed8c6fa6ff19", + "metadata": {}, + "source": [ + "### Compare performance of the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "Finally, use the OpenVINO [Benchmark Tool](https://docs.openvino.ai/2024/learn-openvino/openvino-samples/benchmark-tool.html) to measure the inference performance of the `FP32` and `INT8` models.\n", + "\n", + "> **Note**: For more accurate performance, it is recommended to run `benchmark_app` in a terminal/command prompt after closing other applications. Run `benchmark_app -m -d CPU -shape \"\"` to benchmark async inference on CPU on specific input data shape for one minute. Change `CPU` to `GPU` to benchmark on GPU. Run `benchmark_app --help` to see an overview of all command-line options." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8677df4", + "metadata": {}, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "546828d2", + "metadata": {}, + "outputs": [], + "source": [ + "if int8_model_seg_path.exists():\n", + " !benchmark_app -m $seg_model_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d43554b", + "metadata": {}, + "outputs": [], + "source": [ + "if int8_model_seg_path.exists():\n", + " !benchmark_app -m $int8_model_seg_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "184bcebd-588d-4b4d-a60a-0fa753a07ef9", + "metadata": {}, + "source": [ + "## Other ways to optimize model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The performance could be also improved by another OpenVINO method such as async inference pipeline or preprocessing API.\n", + "\n", + "Async Inference pipeline help to utilize the device more optimal. The key advantage of the Async API is that when a device is busy with inference, the application can perform other tasks in parallel (for example, populating inputs or scheduling other requests) rather than wait for the current inference to complete first. To understand how to perform async inference using openvino, refer to [Async API tutorial](../async-api/async-api.ipynb)\n", + "\n", + "Preprocessing API enables making preprocessing a part of the model reducing application code and dependency on additional image processing libraries. \n", + "The main advantage of Preprocessing API is that preprocessing steps will be integrated into the execution graph and will be performed on a selected device (CPU/GPU etc.) rather than always being executed on CPU as part of an application. This will also improve selected device utilization. For more information, refer to the overview of [Preprocessing API tutorial](../optimize-preprocessing/optimize-preprocessing.ipynb). To see, how it could be used with YOLOV8 object detection model, please, see [Convert and Optimize YOLOv8 real-time object detection with OpenVINO tutorial](./yolov8-object-detection.ipynb)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2e3b4862-c182-4ce4-a473-9f38d98deab8", + "metadata": { + "tags": [] + }, + "source": [ + "## Live demo\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The following code runs model inference on a video:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c7bb92e-e301-45a9-b5ff-f7953fad298f", + "metadata": {}, + "outputs": [], + "source": [ + "import collections\n", + "import time\n", + "import cv2\n", + "from IPython import display\n", + "import numpy as np\n", + "\n", + "\n", + "def run_instance_segmentation(\n", + " source=0,\n", + " flip=False,\n", + " use_popup=False,\n", + " skip_first_frames=0,\n", + " model=seg_model,\n", + " device=device.value,\n", + " video_width=1280,\n", + "):\n", + " player = None\n", + "\n", + " ov_config = {}\n", + " if device != \"CPU\":\n", + " model.reshape({0: [1, 3, 640, 640]})\n", + " if \"GPU\" in device or (\"AUTO\" in device and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + " compiled_model = core.compile_model(model, device, ov_config)\n", + "\n", + " if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + " seg_model.predictor.model.ov_compiled_model = compiled_model\n", + "\n", + " try:\n", + " # Create a video player to play with target fps.\n", + " player = VideoPlayer(source=source, flip=flip, fps=30, skip_first_frames=skip_first_frames, width=video_width)\n", + " # Start capturing.\n", + " player.start()\n", + " if use_popup:\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(winname=title, flags=cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_AUTOSIZE)\n", + "\n", + " processing_times = collections.deque()\n", + " while True:\n", + " # Grab the frame.\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + " # If the frame is larger than video_width, reduce size to improve the performance.\n", + " # If more, increase size for better demo expirience.\n", + " scale = video_width / max(frame.shape)\n", + " frame = cv2.resize(\n", + " src=frame,\n", + " dsize=None,\n", + " fx=scale,\n", + " fy=scale,\n", + " interpolation=cv2.INTER_AREA,\n", + " )\n", + " # Get the results.\n", + " input_image = np.array(frame)\n", + "\n", + " start_time = time.time()\n", + " detections = seg_model(input_image)\n", + " stop_time = time.time()\n", + " frame = detections[0].plot()\n", + "\n", + " processing_times.append(stop_time - start_time)\n", + " # Use processing times from last 200 frames.\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " _, f_width = frame.shape[:2]\n", + " # Mean processing time [ms].\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + " cv2.putText(\n", + " img=frame,\n", + " text=f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " org=(20, 40),\n", + " fontFace=cv2.FONT_HERSHEY_COMPLEX,\n", + " fontScale=f_width / 1000,\n", + " color=(0, 0, 255),\n", + " thickness=1,\n", + " lineType=cv2.LINE_AA,\n", + " )\n", + " # Use this workaround if there is flickering.\n", + " if use_popup:\n", + " cv2.imshow(winname=title, mat=frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # Encode numpy array to jpg.\n", + " _, encoded_img = cv2.imencode(ext=\".jpg\", img=frame, params=[cv2.IMWRITE_JPEG_QUALITY, 100])\n", + " # Create an IPython image.\n", + " i = display.Image(data=encoded_img)\n", + " # Display the image in this notebook.\n", + " display.clear_output(wait=True)\n", + " display.display(i)\n", + " # ctrl-c\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " # any different error\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " if player is not None:\n", + " # Stop capturing.\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "cc5b4ba6-f478-4417-b09d-93fee5adca41", + "metadata": {}, + "source": [ + "### Run Live Object Detection and Segmentation\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Use a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + ">**NOTE**: To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a remote server (for example, in Binder or Google Colab service), the webcam will not work. By default, the lower cell will run model inference on a video file. If you want to try live inference on your webcam set `WEBCAM_INFERENCE = True`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90708017", + "metadata": {}, + "outputs": [], + "source": [ + "WEBCAM_INFERENCE = False\n", + "\n", + "if WEBCAM_INFERENCE:\n", + " VIDEO_SOURCE = 0 # Webcam\n", + "else:\n", + " if not Path(\"people.mp4\").exists():\n", + " download_file(\n", + " \"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/video/people.mp4\",\n", + " \"people.mp4\",\n", + " )\n", + " VIDEO_SOURCE = \"people.mp4\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8f1239c", + "metadata": {}, + "outputs": [], + "source": [ + "device" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "440f2d0e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "run_instance_segmentation(\n", + " source=VIDEO_SOURCE,\n", + " flip=True,\n", + " use_popup=False,\n", + " model=seg_ov_model,\n", + " device=device.value,\n", + " video_width=1280,\n", + ")" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + { modified: 24, original: 24 }, + { modified: 25, original: 25 }, + { modified: 26, original: 26 }, + { modified: 27, original: 27 }, + { modified: 28, original: 28 }, + { modified: 29, original: 29 }, + { modified: 30, original: 30 }, + { modified: 31, original: 31 }, + { modified: 32, original: 32 }, + { modified: 33, original: 33 }, + { modified: 34, original: 34 }, + { modified: 35, original: 35 }, + { modified: 36, original: 36 }, + { modified: 37, original: 37 }, + { modified: 38, original: 38 }, + { modified: 39, original: 39 }, + { modified: 40, original: 40 }, + { modified: 41, original: 41 }, + { modified: 42, original: 42 }, + { modified: 43, original: 43 }, + { modified: 44, original: 44 }, + { modified: 45, original: 45 }, + { modified: 46, original: 46 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 6, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 25, originalLength: 1, modifiedStart: 25, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 42, originalLength: 1, modifiedStart: 42, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 44, originalLength: 1, modifiedStart: 44, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 46, originalLength: 1, modifiedStart: 46, modifiedLength: 1 } satisfies IDiffChange, + ]); + + }); + test('Modification of a large cell and insert a new cell', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "id": "e9d010ff-85ba-4e69-b439-ba89e4a3a715", + "source": [ + "# Convert and Optimize YOLOv10 with OpenVINO\n", + "\n", + "Real-time object detection aims to accurately predict object categories and positions in images with low latency. The YOLO series has been at the forefront of this research due to its balance between performance and efficiency. However, reliance on NMS and architectural inefficiencies have hindered optimal performance. YOLOv10 addresses these issues by introducing consistent dual assignments for NMS-free training and a holistic efficiency-accuracy driven model design strategy.\n", + "\n", + "YOLOv10, built on the [Ultralytics Python package](https://pypi.org/project/ultralytics/) by researchers at [Tsinghua University](https://www.tsinghua.edu.cn/en/), introduces a new approach to real-time object detection, addressing both the post-processing and model architecture deficiencies found in previous YOLO versions. By eliminating non-maximum suppression (NMS) and optimizing various model components, YOLOv10 achieves state-of-the-art performance with significantly reduced computational overhead. Extensive experiments demonstrate its superior accuracy-latency trade-offs across multiple model scales.\n", + "\n", + "![yolov10-approach.png](https://github.com/ultralytics/ultralytics/assets/26833433/f9b1bec0-928e-41ce-a205-e12db3c4929a)\n", + "\n", + "More details about model architecture you can find in original [repo](https://github.com/THU-MIG/yolov10), [paper](https://arxiv.org/abs/2405.14458) and [Ultralytics documentation](https://docs.ultralytics.com/models/yolov10/).\n", + "\n", + "This tutorial demonstrates step-by-step instructions on how to run and optimize PyTorch YOLO V10 with OpenVINO.\n", + "\n", + "The tutorial consists of the following steps:\n", + "\n", + "- Prepare PyTorch model\n", + "- Convert PyTorch model to OpenVINO IR\n", + "- Run model inference with OpenVINO\n", + "- Prepare and run optimization pipeline using NNCF\n", + "- Compare performance of the FP16 and quantized models.\n", + "- Run optimized model inference on video\n", + "- Launch interactive Gradio demo\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Download PyTorch model](#Download-PyTorch-model)\n", + "- [Export PyTorch model to OpenVINO IR Format](#Export-PyTorch-model-to-OpenVINO-IR-Format)\n", + "- [Run OpenVINO Inference on AUTO device using Ultralytics API](#Run-OpenVINO-Inference-on-AUTO-device-using-Ultralytics-API)\n", + "- [Run OpenVINO Inference on selected device using Ultralytics API](#Run-OpenVINO-Inference-on-selected-device-using-Ultralytics-API)\n", + "- [Optimize model using NNCF Post-training Quantization API](#Optimize-model-using-NNCF-Post-training-Quantization-API)\n", + " - [Prepare Quantization Dataset](#Prepare-Quantization-Dataset)\n", + " - [Quantize and Save INT8 model](#Quantize-and-Save-INT8-model)\n", + "- [Run Optimized Model Inference](#Run-Optimized-Model-Inference)\n", + " - [Run Optimized Model on AUTO device](#Run-Optimized-Model-on-AUTO-device)\n", + " - [Run Optimized Model Inference on selected device](#Run-Optimized-Model-Inference-on-selected-device)\n", + "- [Compare the Original and Quantized Models](#Compare-the-Original-and-Quantized-Models)\n", + " - [Model size](#Model-size)\n", + " - [Performance](#Performance)\n", + " - [FP16 model performance](#FP16-model-performance)\n", + " - [Int8 model performance](#Int8-model-performance)\n", + "- [Live demo](#Live-demo)\n", + " - [Gradio Interactive Demo](#Gradio-Interactive-Demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "144e86a1-c220-4e3b-a8b2-8eda443faf2d", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "bf76580b-860a-4dcd-8b01-f494cdffe37e", + "source": [ + "import os\n", + "\n", + "os.environ[\"GIT_CLONE_PROTECTION_ACTIVE\"] = \"false\"\n", + "\n", + "%pip install -q \"nncf>=2.11.0\"\n", + "%pip install -Uq \"openvino>=2024.3.0\"\n", + "%pip install -q \"git+https://github.com/THU-MIG/yolov10.git\" --extra-index-url https://download.pytorch.org/whl/cpu\n", + "%pip install -q \"torch>=2.1\" \"torchvision>=0.16\" tqdm opencv-python \"gradio>=4.19\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "code", + "id": "75772eac-565b-4234-82bf-f305e0c1ceda", + "source": [ + "from pathlib import Path\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "from notebook_utils import download_file, VideoPlayer, device_widget" + ] + }, + { + "cell_type": "markdown", + "id": "2888d8b4-44f6-4418-bae4-f433ff28010b", + "source": [ + "## Download PyTorch model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "There are several version of [YOLO V10](https://github.com/THU-MIG/yolov10/tree/main?tab=readme-ov-file#performance) models provided by model authors. Each of them has different characteristics depends on number of training parameters, performance and accuracy. For demonstration purposes we will use `yolov10n`, but the same steps are also applicable to other models in YOLO V10 series." + ] + }, + { + "cell_type": "code", + "id": "aea6315b-6a29-4f2a-96a9-eb3c1646a3a4", + "source": [ + "models_dir = Path(\"./models\")\n", + "models_dir.mkdir(exist_ok=True)" + ] + }, + { + "cell_type": "code", + "id": "d828401e-b6bd-4796-a7b8-681957a159d4", + "source": [ + "model_weights_url = \"https://github.com/jameslahm/yolov10/releases/download/v1.0/yolov10n.pt\"\n", + "file_name = model_weights_url.split(\"/\")[-1]\n", + "model_name = file_name.replace(\".pt\", \"\")\n", + "\n", + "download_file(model_weights_url, directory=models_dir)" + ] + }, + { + "cell_type": "markdown", + "id": "dc21487d-dd79-4b56-b8e2-5b28c8d92ac8", + "source": [ + "## Export PyTorch model to OpenVINO IR Format\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "markdown", + "id": "8cfec0d6-fb55-4611-861b-326e892fcf09", + "source": [ + "As it was discussed before, YOLO V10 code is designed on top of [Ultralytics](https://docs.ultralytics.com/) library and has similar interface with YOLO V8 (You can check [YOLO V8 notebooks](https://github.com/openvinotoolkit/openvino_notebooks/tree/latest/notebooks/yolov8-optimization) for more detailed instruction how to work with Ultralytics API). Ultralytics support OpenVINO model export using [export](https://docs.ultralytics.com/modes/export/) method of model class. Additionally, we can specify parameters responsible for target input size, static or dynamic input shapes and model precision (FP32/FP16/INT8). INT8 quantization can be additionally performed on export stage, but for making approach more flexible, we consider how to perform quantization using [NNCF](https://github.com/openvinotoolkit/nncf)." + ] + }, + { + "cell_type": "code", + "id": "d488f564-c1d5-4c6a-9b4a-353a3ab749e6", + "source": [ + "import types\n", + "from ultralytics.utils import ops, yaml_load, yaml_save\n", + "from ultralytics import YOLOv10\n", + "import torch\n", + "\n", + "detection_labels = {\n", + " 0: \"person\",\n", + " 1: \"bicycle\",\n", + " 2: \"car\",\n", + " 3: \"motorcycle\",\n", + " 4: \"airplane\",\n", + " 5: \"bus\",\n", + " 6: \"train\",\n", + " 7: \"truck\",\n", + " 8: \"boat\",\n", + " 9: \"traffic light\",\n", + " 10: \"fire hydrant\",\n", + " 11: \"stop sign\",\n", + " 12: \"parking meter\",\n", + " 13: \"bench\",\n", + " 14: \"bird\",\n", + " 15: \"cat\",\n", + " 16: \"dog\",\n", + " 17: \"horse\",\n", + " 18: \"sheep\",\n", + " 19: \"cow\",\n", + " 20: \"elephant\",\n", + " 21: \"bear\",\n", + " 22: \"zebra\",\n", + " 23: \"giraffe\",\n", + " 24: \"backpack\",\n", + " 25: \"umbrella\",\n", + " 26: \"handbag\",\n", + " 27: \"tie\",\n", + " 28: \"suitcase\",\n", + " 29: \"frisbee\",\n", + " 30: \"skis\",\n", + " 31: \"snowboard\",\n", + " 32: \"sports ball\",\n", + " 33: \"kite\",\n", + " 34: \"baseball bat\",\n", + " 35: \"baseball glove\",\n", + " 36: \"skateboard\",\n", + " 37: \"surfboard\",\n", + " 38: \"tennis racket\",\n", + " 39: \"bottle\",\n", + " 40: \"wine glass\",\n", + " 41: \"cup\",\n", + " 42: \"fork\",\n", + " 43: \"knife\",\n", + " 44: \"spoon\",\n", + " 45: \"bowl\",\n", + " 46: \"banana\",\n", + " 47: \"apple\",\n", + " 48: \"sandwich\",\n", + " 49: \"orange\",\n", + " 50: \"broccoli\",\n", + " 51: \"carrot\",\n", + " 52: \"hot dog\",\n", + " 53: \"pizza\",\n", + " 54: \"donut\",\n", + " 55: \"cake\",\n", + " 56: \"chair\",\n", + " 57: \"couch\",\n", + " 58: \"potted plant\",\n", + " 59: \"bed\",\n", + " 60: \"dining table\",\n", + " 61: \"toilet\",\n", + " 62: \"tv\",\n", + " 63: \"laptop\",\n", + " 64: \"mouse\",\n", + " 65: \"remote\",\n", + " 66: \"keyboard\",\n", + " 67: \"cell phone\",\n", + " 68: \"microwave\",\n", + " 69: \"oven\",\n", + " 70: \"toaster\",\n", + " 71: \"sink\",\n", + " 72: \"refrigerator\",\n", + " 73: \"book\",\n", + " 74: \"clock\",\n", + " 75: \"vase\",\n", + " 76: \"scissors\",\n", + " 77: \"teddy bear\",\n", + " 78: \"hair drier\",\n", + " 79: \"toothbrush\",\n", + "}\n", + "\n", + "\n", + "def v10_det_head_forward(self, x):\n", + " one2one = self.forward_feat([xi.detach() for xi in x], self.one2one_cv2, self.one2one_cv3)\n", + " if not self.export:\n", + " one2many = super().forward(x)\n", + "\n", + " if not self.training:\n", + " one2one = self.inference(one2one)\n", + " if not self.export:\n", + " return {\"one2many\": one2many, \"one2one\": one2one}\n", + " else:\n", + " assert self.max_det != -1\n", + " boxes, scores, labels = ops.v10postprocess(one2one.permute(0, 2, 1), self.max_det, self.nc)\n", + " return torch.cat(\n", + " [boxes, scores.unsqueeze(-1), labels.unsqueeze(-1).to(boxes.dtype)],\n", + " dim=-1,\n", + " )\n", + " else:\n", + " return {\"one2many\": one2many, \"one2one\": one2one}\n", + "\n", + "\n", + "ov_model_path = models_dir / f\"{model_name}_openvino_model/{model_name}.xml\"\n", + "if not ov_model_path.exists():\n", + " model = YOLOv10(models_dir / file_name)\n", + " model.model.model[-1].forward = types.MethodType(v10_det_head_forward, model.model.model[-1])\n", + " model.export(format=\"openvino\", dynamic=True, half=True)\n", + " config = yaml_load(ov_model_path.parent / \"metadata.yaml\")\n", + " config[\"names\"] = detection_labels\n", + " yaml_save(ov_model_path.parent / \"metadata.yaml\", config)" + ] + }, + { + "cell_type": "markdown", + "id": "8a23f621-f6cd-4bfd-8b98-24b8c0392761", + "source": [ + "## Run OpenVINO Inference on AUTO device using Ultralytics API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "Now, when we exported model to OpenVINO, we can load it directly into YOLOv10 class, where automatic inference backend will provide easy-to-use user experience to run OpenVINO YOLOv10 model on the similar level like for original PyTorch model. The code bellow demonstrates how to run inference OpenVINO exported model with Ultralytics API on single image. [AUTO device](https://github.com/openvinotoolkit/openvino_notebooks/tree/latest/notebooks/auto-device) will be used for launching model." + ] + }, + { + "cell_type": "code", + "id": "7e8ed361-bf09-4398-ba15-e6c5dda73cd3", + "source": [ + "ov_yolo_model = YOLOv10(ov_model_path.parent, task=\"detect\")" + ] + }, + { + "cell_type": "code", + "id": "8a473286-59b8-498f-8541-df84f041e119", + "source": [ + "from PIL import Image\n", + "\n", + "IMAGE_PATH = Path(\"./data/coco_bike.jpg\")\n", + "download_file(\n", + " url=\"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/image/coco_bike.jpg\",\n", + " filename=IMAGE_PATH.name,\n", + " directory=IMAGE_PATH.parent,\n", + ")" + ] + }, + { + "cell_type": "code", + "id": "7e116e81-0ad4-4b9e-9f5c-58680fd8f0c6", + "source": [ + "res = ov_yolo_model(IMAGE_PATH, iou=0.45, conf=0.2)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "943b9b04-4b97-4395-99d1-695465de1140", + "source": [ + "## Run OpenVINO Inference on selected device using Ultralytics API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "In this part of notebook you can select inference device for running model inference to compare results with AUTO." + ] + }, + { + "cell_type": "code", + "id": "41655bec-b006-4376-abbb-d6c8c09e89fb", + "source": [ + "device = device_widget(\"CPU\")\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "f284d5fd-12ba-4c08-afce-d8131ef7ad4d", + "source": [ + "import openvino as ov\n", + "\n", + "core = ov.Core()\n", + "\n", + "ov_model = core.read_model(ov_model_path)\n", + "\n", + "# load model on selected device\n", + "if \"GPU\" in device.value or \"NPU\" in device.value:\n", + " ov_model.reshape({0: [1, 3, 640, 640]})\n", + "ov_config = {}\n", + "if \"GPU\" in device.value:\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "det_compiled_model = core.compile_model(ov_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "id": "12164d21-948a-4ef7-9e1c-b0d41c53cf91", + "source": [ + "ov_yolo_model.predictor.model.ov_compiled_model = det_compiled_model" + ] + }, + { + "cell_type": "code", + "id": "ecb9d396-ab1e-4398-a110-18c08b00ab14", + "source": [ + "res = ov_yolo_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "a4abf758-3a72-4ee0-afde-38076602f1c7", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "992b6ac3-4248-4848-8fd6-f9c1b9fc2051", + "source": [ + "## Optimize model using NNCF Post-training Quantization API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "[NNCF](https://github.com/openvinotoolkit/nncf) provides a suite of advanced algorithms for Neural Networks inference optimization in OpenVINO with minimal accuracy drop.\n", + "We will use 8-bit quantization in post-training mode (without the fine-tuning pipeline) to optimize YOLOv10.\n", + "\n", + "The optimization process contains the following steps:\n", + "\n", + "1. Create a Dataset for quantization.\n", + "2. Run `nncf.quantize` for getting an optimized model.\n", + "3. Serialize OpenVINO IR model, using the `openvino.save_model` function.\n", + "\n", + "Quantization is time and memory consuming process, you can skip this step using checkbox bellow:" + ] + }, + { + "cell_type": "code", + "id": "ed9d9ce5-8df9-4803-ae85-bf0744de914c", + "source": [ + "import ipywidgets as widgets\n", + "\n", + "int8_model_det_path = models_dir / \"int8\" / f\"{model_name}_openvino_model/{model_name}.xml\"\n", + "ov_yolo_int8_model = None\n", + "\n", + "to_quantize = widgets.Checkbox(\n", + " value=True,\n", + " description=\"Quantization\",\n", + " disabled=False,\n", + ")\n", + "\n", + "to_quantize" + ] + }, + { + "cell_type": "code", + "id": "52287a23-b5ef-4c34-873a-32b398d26d1a", + "source": [ + "# Fetch skip_kernel_extension module\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/skip_kernel_extension.py\",\n", + ")\n", + "open(\"skip_kernel_extension.py\", \"w\").write(r.text)\n", + "\n", + "%load_ext skip_kernel_extension" + ] + }, + { + "cell_type": "markdown", + "id": "89ba64af-aa73-4ed9-a2f9-527bd9ae8221", + "source": [ + "### Prepare Quantization Dataset\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "For starting quantization, we need to prepare dataset. We will use validation subset from [MS COCO dataset](https://cocodataset.org/) for model quantization and Ultralytics validation data loader for preparing input data." + ] + }, + { + "cell_type": "code", + "id": "5e10435e-9cd6-4f29-a865-721d6209314d", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "from zipfile import ZipFile\n", + "\n", + "from ultralytics.data.utils import DATASETS_DIR\n", + "\n", + "if not int8_model_det_path.exists():\n", + "\n", + " DATA_URL = \"http://images.cocodataset.org/zips/val2017.zip\"\n", + " LABELS_URL = \"https://github.com/ultralytics/yolov5/releases/download/v1.0/coco2017labels-segments.zip\"\n", + " CFG_URL = \"https://raw.githubusercontent.com/ultralytics/ultralytics/v8.1.0/ultralytics/cfg/datasets/coco.yaml\"\n", + "\n", + " OUT_DIR = DATASETS_DIR\n", + "\n", + " DATA_PATH = OUT_DIR / \"val2017.zip\"\n", + " LABELS_PATH = OUT_DIR / \"coco2017labels-segments.zip\"\n", + " CFG_PATH = OUT_DIR / \"coco.yaml\"\n", + "\n", + " download_file(DATA_URL, DATA_PATH.name, DATA_PATH.parent)\n", + " download_file(LABELS_URL, LABELS_PATH.name, LABELS_PATH.parent)\n", + " download_file(CFG_URL, CFG_PATH.name, CFG_PATH.parent)\n", + "\n", + " if not (OUT_DIR / \"coco/labels\").exists():\n", + " with ZipFile(LABELS_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR)\n", + " with ZipFile(DATA_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR / \"coco/images\")" + ] + }, + { + "cell_type": "code", + "id": "b0a14818-21fd-4c71-8e58-2297dfafaadd", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "from ultralytics.utils import DEFAULT_CFG\n", + "from ultralytics.cfg import get_cfg\n", + "from ultralytics.data.converter import coco80_to_coco91_class\n", + "from ultralytics.data.utils import check_det_dataset\n", + "\n", + "if not int8_model_det_path.exists():\n", + " args = get_cfg(cfg=DEFAULT_CFG)\n", + " args.data = str(CFG_PATH)\n", + " det_validator = ov_yolo_model.task_map[ov_yolo_model.task][\"validator\"](args=args)\n", + "\n", + " det_validator.data = check_det_dataset(args.data)\n", + " det_validator.stride = 32\n", + " det_data_loader = det_validator.get_dataloader(OUT_DIR / \"coco\", 1)" + ] + }, + { + "cell_type": "markdown", + "id": "83fa49f4-e40c-48c7-82a6-8ef56e20b343", + "source": [ + "NNCF provides `nncf.Dataset` wrapper for using native framework dataloaders in quantization pipeline. Additionally, we specify transform function that will be responsible for preparing input data in model expected format." + ] + }, + { + "cell_type": "code", + "id": "253f0e0f-131e-43b9-b1be-639d4b3d1fe5", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "import nncf\n", + "from typing import Dict\n", + "\n", + "\n", + "def transform_fn(data_item:Dict):\n", + " \"\"\"\n", + " Quantization transform function. Extracts and preprocess input data from dataloader item for quantization.\n", + " Parameters:\n", + " data_item: Dict with data item produced by DataLoader during iteration\n", + " Returns:\n", + " input_tensor: Input data for quantization\n", + " \"\"\"\n", + " input_tensor = det_validator.preprocess(data_item)['img'].numpy()\n", + " return input_tensor\n", + "\n", + "if not int8_model_det_path.exists():\n", + " quantization_dataset = nncf.Dataset(det_data_loader, transform_fn)" + ] + }, + { + "cell_type": "markdown", + "id": "ff8a6372-2097-4308-a4e5-e4651242a8df", + "source": [ + "### Quantize and Save INT8 model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The `nncf.quantize` function provides an interface for model quantization. It requires an instance of the OpenVINO Model and quantization dataset. \n", + "Optionally, some additional parameters for the configuration quantization process (number of samples for quantization, preset, ignored scope, etc.) can be provided. YOLOv10 model contains non-ReLU activation functions, which require asymmetric quantization of activations. To achieve a better result, we will use a `mixed` quantization preset. It provides symmetric quantization of weights and asymmetric quantization of activations.\n", + "\n", + ">**Note**: Model post-training quantization is time-consuming process. Be patient, it can take several minutes depending on your hardware." + ] + }, + { + "cell_type": "code", + "id": "4c12ae38-239e-4fe1-8296-47567f98538c", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "import shutil\n", + "\n", + "if not int8_model_det_path.exists():\n", + " quantized_det_model = nncf.quantize(\n", + " ov_model,\n", + " quantization_dataset,\n", + " preset=nncf.QuantizationPreset.MIXED,\n", + " )\n", + "\n", + " ov.save_model(quantized_det_model, int8_model_det_path)\n", + " shutil.copy(ov_model_path.parent / \"metadata.yaml\", int8_model_det_path.parent / \"metadata.yaml\")" + ] + }, + { + "cell_type": "markdown", + "id": "41b8a3eb-adb8-40cf-8602-311ff8d75a76", + "source": [ + "## Run Optimized Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The way of usage INT8 quantized model is the same like for model before quantization. Let's check inference result of quantized model on single image" + ] + }, + { + "cell_type": "markdown", + "id": "f438df41-d614-4cca-a746-a173d92dddf5", + "source": [ + "### Run Optimized Model on AUTO device\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "af61303b-16bd-4206-b926-2c28b3a859a4", + "source": [ + "%%skip not $to_quantize.value\n", + "ov_yolo_int8_model = YOLOv10(int8_model_det_path.parent, task=\"detect\")" + ] + }, + { + "cell_type": "code", + "id": "db13b8dd-49a1-4046-8a1a-491eee095a1b", + "source": [ + "%%skip not $to_quantize.value\n", + "res = ov_yolo_int8_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "0423ba31-f0ee-4bfc-a18a-f7b870a9683d", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "4b4f7118-e046-4f87-b403-87053f2b0ac3", + "source": [ + "### Run Optimized Model Inference on selected device\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "142f22ae-6a8d-4578-b97f-4d9c77123418", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "2f43767a-1994-4202-a411-4738c92223da", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "ov_config = {}\n", + "if \"GPU\" in device.value or \"NPU\" in device.value:\n", + " ov_model.reshape({0: [1, 3, 640, 640]})\n", + "ov_config = {}\n", + "if \"GPU\" in device.value:\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "\n", + "quantized_det_model = core.read_model(int8_model_det_path)\n", + "quantized_det_compiled_model = core.compile_model(quantized_det_model, device.value, ov_config)\n", + "\n", + "ov_yolo_int8_model.predictor.model.ov_compiled_model = quantized_det_compiled_model\n", + "\n", + "res = ov_yolo_int8_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "51d64c7b-7595-41ca-87dd-4c85308f84d6", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "c7b70f75-a706-4f24-a487-88a3ad645dd5", + "source": [ + "## Compare the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "markdown", + "id": "d6561246-a9e0-4cdf-8207-7e2f919d8582", + "source": [ + "### Model size\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "43d84d15-75be-4430-a58b-61cfd8e08dd0", + "source": [ + "ov_model_weights = ov_model_path.with_suffix(\".bin\")\n", + "print(f\"Size of FP16 model is {ov_model_weights.stat().st_size / 1024 / 1024:.2f} MB\")\n", + "if int8_model_det_path.exists():\n", + " ov_int8_weights = int8_model_det_path.with_suffix(\".bin\")\n", + " print(f\"Size of model with INT8 compressed weights is {ov_int8_weights.stat().st_size / 1024 / 1024:.2f} MB\")\n", + " print(f\"Compression rate for INT8 model: {ov_model_weights.stat().st_size / ov_int8_weights.stat().st_size:.3f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f72b3d84-91f3-493c-8f4e-46598ffbd6d1", + "source": [ + "### Performance\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "### FP16 model performance\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "909ea508-e007-4313-ab0f-bfa0e3906176", + "source": [ + "!benchmark_app -m $ov_model_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "markdown", + "id": "9852a8ef-4c45-43dd-a9ef-53ca6f13c99e", + "source": [ + "### Int8 model performance\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "8254626d-6c4f-4ca3-936f-a7c47e402e03", + "source": [ + "if int8_model_det_path.exists():\n", + " !benchmark_app -m $int8_model_det_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "markdown", + "id": "12d35bc1-8101-45ea-8bd7-53720b98f5e5", + "source": [ + "## Live demo\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The following code runs model inference on a video:" + ] + }, + { + "cell_type": "code", + "id": "246bd379-b1d5-4eb2-928f-97c7c2455a92", + "source": [ + "import collections\n", + "import time\n", + "from IPython import display\n", + "import cv2\n", + "import numpy as np\n", + "\n", + "\n", + "# Main processing function to run object detection.\n", + "def run_object_detection(\n", + " source=0,\n", + " flip=False,\n", + " use_popup=False,\n", + " skip_first_frames=0,\n", + " det_model=ov_yolo_int8_model,\n", + " device=device.value,\n", + "):\n", + " player = None\n", + " try:\n", + " # Create a video player to play with target fps.\n", + " player = VideoPlayer(source=source, flip=flip, fps=30, skip_first_frames=skip_first_frames)\n", + " # Start capturing.\n", + " player.start()\n", + " if use_popup:\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(winname=title, flags=cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_AUTOSIZE)\n", + "\n", + " processing_times = collections.deque()\n", + " while True:\n", + " # Grab the frame.\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + " # If the frame is larger than full HD, reduce size to improve the performance.\n", + " scale = 1280 / max(frame.shape)\n", + " if scale < 1:\n", + " frame = cv2.resize(\n", + " src=frame,\n", + " dsize=None,\n", + " fx=scale,\n", + " fy=scale,\n", + " interpolation=cv2.INTER_AREA,\n", + " )\n", + " # Get the results.\n", + " input_image = np.array(frame)\n", + "\n", + " start_time = time.time()\n", + " detections = det_model(input_image, iou=0.45, conf=0.2, verbose=False)\n", + " stop_time = time.time()\n", + " frame = detections[0].plot()\n", + "\n", + " processing_times.append(stop_time - start_time)\n", + " # Use processing times from last 200 frames.\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " _, f_width = frame.shape[:2]\n", + " # Mean processing time [ms].\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + " cv2.putText(\n", + " img=frame,\n", + " text=f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " org=(20, 40),\n", + " fontFace=cv2.FONT_HERSHEY_COMPLEX,\n", + " fontScale=f_width / 1000,\n", + " color=(0, 0, 255),\n", + " thickness=1,\n", + " lineType=cv2.LINE_AA,\n", + " )\n", + " # Use this workaround if there is flickering.\n", + " if use_popup:\n", + " cv2.imshow(winname=title, mat=frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # Encode numpy array to jpg.\n", + " _, encoded_img = cv2.imencode(ext=\".jpg\", img=frame, params=[cv2.IMWRITE_JPEG_QUALITY, 100])\n", + " # Create an IPython image.\n", + " i = display.Image(data=encoded_img)\n", + " # Display the image in this notebook.\n", + " display.clear_output(wait=True)\n", + " display.display(i)\n", + " # ctrl-c\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " # any different error\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " if player is not None:\n", + " # Stop capturing.\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()" + ] + }, + { + "cell_type": "code", + "id": "8db930f4-fc41-4635-9248-deab2b6cfcb8", + "source": [ + "use_int8 = widgets.Checkbox(\n", + " value=ov_yolo_int8_model is not None,\n", + " description=\"Use int8 model\",\n", + " disabled=ov_yolo_int8_model is None,\n", + ")\n", + "\n", + "use_int8" + ] + }, + { + "cell_type": "code", + "id": "59e37135-1bdc-40ff-8789-b5b4491cab84", + "source": [ + "WEBCAM_INFERENCE = False\n", + "\n", + "if WEBCAM_INFERENCE:\n", + " VIDEO_SOURCE = 0 # Webcam\n", + "else:\n", + " download_file(\n", + " \"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/video/people.mp4\",\n", + " directory=\"data\",\n", + " )\n", + " VIDEO_SOURCE = \"data/people.mp4\"" + ] + }, + { + "cell_type": "code", + "id": "88c6f47c-1c47-451d-89ba-0a29760f5ec4", + "source": [ + "run_object_detection(\n", + " det_model=ov_yolo_model if not use_int8.value else ov_yolo_int8_model,\n", + " source=VIDEO_SOURCE,\n", + " flip=True,\n", + " use_popup=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9f612d80-68e2-45f8-aa62-9f3a05ca9de8", + "source": [ + "### Gradio Interactive Demo\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "ffdbe248-7086-4804-b4b9-c3dd454bf80c", + "source": [ + "import gradio as gr\n", + "\n", + "\n", + "def yolov10_inference(image, int8, conf_threshold, iou_threshold):\n", + " model = ov_yolo_model if not int8 else ov_yolo_int8_model\n", + " results = model(source=image, iou=iou_threshold, conf=conf_threshold, verbose=False)[0]\n", + " annotated_image = Image.fromarray(results.plot())\n", + "\n", + " return annotated_image\n", + "\n", + "\n", + "with gr.Blocks() as demo:\n", + " gr.HTML(\n", + " \"\"\"\n", + "

\n", + " YOLOv10: Real-Time End-to-End Object Detection using OpenVINO\n", + "

\n", + " \"\"\"\n", + " )\n", + " with gr.Row():\n", + " with gr.Column():\n", + " image = gr.Image(type=\"numpy\", label=\"Image\")\n", + " conf_threshold = gr.Slider(\n", + " label=\"Confidence Threshold\",\n", + " minimum=0.1,\n", + " maximum=1.0,\n", + " step=0.1,\n", + " value=0.2,\n", + " )\n", + " iou_threshold = gr.Slider(\n", + " label=\"IoU Threshold\",\n", + " minimum=0.1,\n", + " maximum=1.0,\n", + " step=0.1,\n", + " value=0.45,\n", + " )\n", + " use_int8 = gr.Checkbox(\n", + " value=ov_yolo_int8_model is not None,\n", + " visible=ov_yolo_int8_model is not None,\n", + " label=\"Use INT8 model\",\n", + " )\n", + " yolov10_infer = gr.Button(value=\"Detect Objects\")\n", + "\n", + " with gr.Column():\n", + " output_image = gr.Image(type=\"pil\", label=\"Annotated Image\")\n", + "\n", + " yolov10_infer.click(\n", + " fn=yolov10_inference,\n", + " inputs=[\n", + " image,\n", + " use_int8,\n", + " conf_threshold,\n", + " iou_threshold,\n", + " ],\n", + " outputs=[output_image],\n", + " )\n", + " examples = gr.Examples(\n", + " [\n", + " \"data/coco_bike.jpg\",\n", + " ],\n", + " inputs=[\n", + " image,\n", + " ],\n", + " )\n", + "\n", + "\n", + "try:\n", + " demo.launch(debug=True)\n", + "except Exception:\n", + " demo.launch(debug=True, share=True)" + ] + } + ] + .map(fromJupyterCell) + , + [ + { + "cell_type": "markdown", + "id": "e9d010ff-85ba-4e69-b439-ba89e4a3a715", + "source": [ + "# Convert and Optimize YOLOv10 with OpenVINO\n", + "\n", + "Real-time object detection aims to accurately predict object categories and positions in images with low latency. The YOLO series has been at the forefront of this research due to its balance between performance and efficiency. However, reliance on NMS and architectural inefficiencies have hindered optimal performance. YOLOv10 addresses these issues by introducing consistent dual assignments for NMS-free training and a holistic efficiency-accuracy driven model design strategy.\n", + "\n", + "YOLOv10, built on the [Ultralytics Python package](https://pypi.org/project/ultralytics/) by researchers at [Tsinghua University](https://www.tsinghua.edu.cn/en/), introduces a new approach to real-time object detection, addressing both the post-processing and model architecture deficiencies found in previous YOLO versions. By eliminating non-maximum suppression (NMS) and optimizing various model components, YOLOv10 achieves state-of-the-art performance with significantly reduced computational overhead. Extensive experiments demonstrate its superior accuracy-latency trade-offs across multiple model scales.\n", + "\n", + "![yolov10-approach.png](https://github.com/ultralytics/ultralytics/assets/26833433/f9b1bec0-928e-41ce-a205-e12db3c4929a)\n", + "\n", + "More details about model architecture you can find in original [repo](https://github.com/THU-MIG/yolov10), [paper](https://arxiv.org/abs/2405.14458) and [Ultralytics documentation](https://docs.ultralytics.com/models/yolov10/).\n", + "\n", + "This tutorial demonstrates step-by-step instructions on how to run and optimize PyTorch YOLO V10 with OpenVINO.\n", + "\n", + "The tutorial consists of the following steps:\n", + "\n", + "- Prepare PyTorch model\n", + "- Convert PyTorch model to OpenVINO IR\n", + "- Run model inference with OpenVINO\n", + "- Prepare and run optimization pipeline using NNCF\n", + "- Compare performance of the FP16 and quantized models.\n", + "- Run optimized model inference on video\n", + "- Launch interactive Gradio demo\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Download PyTorch model](#Download-PyTorch-model)\n", + "- [Export PyTorch model to OpenVINO IR Format](#Export-PyTorch-model-to-OpenVINO-IR-Format)\n", + "- [Run OpenVINO Inference on AUTO device using Ultralytics API](#Run-OpenVINO-Inference-on-AUTO-device-using-Ultralytics-API)\n", + "- [Run OpenVINO Inference on selected device using Ultralytics API](#Run-OpenVINO-Inference-on-selected-device-using-Ultralytics-API)\n", + "- [Optimize model using NNCF Post-training Quantization API](#Optimize-model-using-NNCF-Post-training-Quantization-API)\n", + " - [Prepare Quantization Dataset](#Prepare-Quantization-Dataset)\n", + " - [Quantize and Save INT8 model](#Quantize-and-Save-INT8-model)\n", + "- [Run Optimized Model Inference](#Run-Optimized-Model-Inference)\n", + " - [Run Optimized Model on AUTO device](#Run-Optimized-Model-on-AUTO-device)\n", + " - [Run Optimized Model Inference on selected device](#Run-Optimized-Model-Inference-on-selected-device)\n", + "- [Compare the Original and Quantized Models](#Compare-the-Original-and-Quantized-Models)\n", + " - [Model size](#Model-size)\n", + " - [Performance](#Performance)\n", + " - [FP16 model performance](#FP16-model-performance)\n", + " - [Int8 model performance](#Int8-model-performance)\n", + "- [Live demo](#Live-demo)\n", + " - [Gradio Interactive Demo](#Gradio-Interactive-Demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "144e86a1-c220-4e3b-a8b2-8eda443faf2d", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "bf76580b-860a-4dcd-8b01-f494cdffe37e", + "source": [ + "import os\n", + "\n", + "os.environ[\"GIT_CLONE_PROTECTION_ACTIVE\"] = \"false\"\n", + "\n", + "%pip install -q \"nncf>=2.11.0\"\n", + "%pip install -Uq \"openvino>=2024.3.0\"\n", + "%pip install -q \"git+https://github.com/THU-MIG/yolov10.git\" --extra-index-url https://download.pytorch.org/whl/cpu\n", + "%pip install -q \"torch>=2.1\" \"torchvision>=0.16\" tqdm opencv-python \"gradio>=4.19\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "code", + "id": "75772eac-565b-4234-82bf-f305e0c1ceda", + "source": [ + "from pathlib import Path\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "from notebook_utils import download_file, VideoPlayer, device_widget" + ] + }, + { + "cell_type": "markdown", + "id": "2888d8b4-44f6-4418-bae4-f433ff28010b", + "source": [ + "## Download PyTorch model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "There are several version of [YOLO V10](https://github.com/THU-MIG/yolov10/tree/main?tab=readme-ov-file#performance) models provided by model authors. Each of them has different characteristics depends on number of training parameters, performance and accuracy. For demonstration purposes we will use `yolov10n`, but the same steps are also applicable to other models in YOLO V10 series." + ] + }, + { + "cell_type": "code", + "id": "aea6315b-6a29-4f2a-96a9-eb3c1646a3a4", + "source": [ + "models_dir = Path(\"./models\")\n", + "models_dir.mkdir(exist_ok=True)" + ] + }, + { + "cell_type": "code", + "id": "d828401e-b6bd-4796-a7b8-681957a159d4", + "source": [ + "model_weights_url = \"https://github.com/jameslahm/yolov10/releases/download/v1.0/yolov10n.pt\"\n", + "file_name = model_weights_url.split(\"/\")[-1]\n", + "model_name = file_name.replace(\".pt\", \"\")\n", + "\n", + "download_file(model_weights_url, directory=models_dir)" + ] + }, + { + "cell_type": "markdown", + "id": "dc21487d-dd79-4b56-b8e2-5b28c8d92ac8", + "source": [ + "## Export PyTorch model to OpenVINO IR Format\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "markdown", + "id": "8cfec0d6-fb55-4611-861b-326e892fcf09", + "source": [ + "As it was discussed before, YOLO V10 code is designed on top of [Ultralytics](https://docs.ultralytics.com/) library and has similar interface with YOLO V8 (You can check [YOLO V8 notebooks](https://github.com/openvinotoolkit/openvino_notebooks/tree/latest/notebooks/yolov8-optimization) for more detailed instruction how to work with Ultralytics API). Ultralytics support OpenVINO model export using [export](https://docs.ultralytics.com/modes/export/) method of model class. Additionally, we can specify parameters responsible for target input size, static or dynamic input shapes and model precision (FP32/FP16/INT8). INT8 quantization can be additionally performed on export stage, but for making approach more flexible, we consider how to perform quantization using [NNCF](https://github.com/openvinotoolkit/nncf)." + ] + }, + { + "cell_type": "code", + "id": "d488f564-c1d5-4c6a-9b4a-353a3ab749e6", + "source": [ + "import types\n", + "from ultralytics.utils import ops, yaml_load, yaml_save\n", + "from ultralytics import YOLOv10\n", + "import torch\n", + "\n", + "detection_labels = {\n", + " 0: \"person\",\n", + " 1: \"bicycle\",\n", + " 2: \"car\",\n", + " 3: \"motorcycle\",\n", + " 4: \"airplane\",\n", + " 5: \"bus\",\n", + " 6: \"train\",\n", + " 7: \"truck\",\n", + " 8: \"boat\",\n", + " 9: \"traffic light\",\n", + " 10: \"fire hydrant\",\n", + " 11: \"stop sign\",\n", + " 12: \"parking meter\",\n", + " 13: \"bench\",\n", + " 14: \"bird\",\n", + " 15: \"cat\",\n", + " 16: \"dog\",\n", + " 17: \"horse\",\n", + " 18: \"sheep\",\n", + " 19: \"cow\",\n", + " 20: \"elephant\",\n", + " 21: \"bear\",\n", + " 22: \"zebra\",\n", + " 23: \"giraffe\",\n", + " 24: \"backpack\",\n", + " 25: \"umbrella\",\n", + " 26: \"handbag\",\n", + " 27: \"tie\",\n", + " 28: \"suitcase\",\n", + " 29: \"frisbee\",\n", + " 30: \"skis\",\n", + " 31: \"snowboard\",\n", + " 32: \"sports ball\",\n", + " 33: \"kite\",\n", + " 34: \"baseball bat\",\n", + " 35: \"baseball glove\",\n", + " 36: \"skateboard\",\n", + " 37: \"surfboard\",\n", + " 38: \"tennis racket\",\n", + " 39: \"bottle\",\n", + " 40: \"wine glass\",\n", + " 41: \"cup\",\n", + " 42: \"fork\",\n", + " 43: \"knife\",\n", + " 44: \"spoon\",\n", + " 45: \"bowl\",\n", + " 46: \"banana\",\n", + " 47: \"apple\",\n", + " 48: \"sandwich\",\n", + " 49: \"orange\",\n", + " 50: \"broccoli\",\n", + " 51: \"carrot\",\n", + " 52: \"hot dog\",\n", + " 53: \"pizza\",\n", + " 54: \"donut\",\n", + " 55: \"cake\",\n", + " 56: \"chair\",\n", + " 57: \"couch\",\n", + " 58: \"potted plant\",\n", + " 59: \"bed\",\n", + " 60: \"dining table\",\n", + " 61: \"toilet\",\n", + " 62: \"tv\",\n", + " 63: \"laptop\",\n", + " 64: \"mouse\",\n", + " 65: \"remote\",\n", + " 66: \"keyboard\",\n", + " 67: \"cell phone\",\n", + " 68: \"microwave\",\n", + " 69: \"oven\",\n", + " 70: \"toaster\",\n", + " 71: \"sink\",\n", + " 72: \"refrigerator\",\n", + " 73: \"book\",\n", + " 74: \"clock\",\n", + " 75: \"vase\",\n", + " 76: \"scissors\",\n", + " 77: \"teddy bear\",\n", + " 78: \"hair drier\",\n", + " 79: \"toothbrush\",\n", + "}\n", + "\n", + "\n", + "def v10_det_head_forward(self, x):\n", + " one2one = self.forward_feat([xi.detach() for xi in x], self.one2one_cv2, self.one2one_cv3)\n", + " if not self.export:\n", + " one2many = super().forward(x)\n", + "\n", + " if not self.training:\n", + " one2one = self.inference(one2one)\n", + " if not self.export:\n", + " return {\"one2many\": one2many, \"one2one\": one2one}\n", + " else:\n", + " assert self.max_det != -1\n", + " boxes, scores, labels = ops.v10postprocess(one2one.permute(0, 2, 1), self.max_det, self.nc)\n", + " return torch.cat(\n", + " [boxes, scores.unsqueeze(-1), labels.unsqueeze(-1).to(boxes.dtype)],\n", + " dim=-1,\n", + " )\n", + " else:\n", + " return {\"one2many\": one2many, \"one2one\": one2one}\n", + "\n", + "\n", + "ov_model_path = models_dir / f\"{model_name}_openvino_model/{model_name}.xml\"\n", + "if not ov_model_path.exists():\n", + " model = YOLOv10(models_dir / file_name)\n", + " model.model.model[-1].forward = types.MethodType(v10_det_head_forward, model.model.model[-1])\n", + " model.export(format=\"openvino\", dynamic=True, half=True)\n", + " config = yaml_load(ov_model_path.parent / \"metadata.yaml\")\n", + " config[\"names\"] = detection_labels\n", + " yaml_save(ov_model_path.parent / \"metadata.yaml\", config)" + ] + }, + { + "cell_type": "markdown", + "id": "8a23f621-f6cd-4bfd-8b98-24b8c0392761", + "source": [ + "## Run OpenVINO Inference on AUTO device using Ultralytics API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "Now, when we exported model to OpenVINO, we can load it directly into YOLOv10 class, where automatic inference backend will provide easy-to-use user experience to run OpenVINO YOLOv10 model on the similar level like for original PyTorch model. The code bellow demonstrates how to run inference OpenVINO exported model with Ultralytics API on single image. [AUTO device](https://github.com/openvinotoolkit/openvino_notebooks/tree/latest/notebooks/auto-device) will be used for launching model." + ] + }, + { + "cell_type": "code", + "id": "7e8ed361-bf09-4398-ba15-e6c5dda73cd3", + "source": [ + "ov_yolo_model = YOLOv10(ov_model_path.parent, task=\"detect\")" + ] + }, + { + "cell_type": "code", + "id": "8a473286-59b8-498f-8541-df84f041e119", + "source": [ + "from PIL import Image\n", + "\n", + "IMAGE_PATH = Path(\"./data/coco_bike.jpg\")\n", + "download_file(\n", + " url=\"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/image/coco_bike.jpg\",\n", + " filename=IMAGE_PATH.name,\n", + " directory=IMAGE_PATH.parent,\n", + ")" + ] + }, + { + "cell_type": "code", + "id": "7e116e81-0ad4-4b9e-9f5c-58680fd8f0c6", + "source": [ + "res = ov_yolo_model(IMAGE_PATH, iou=0.45, conf=0.2)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "943b9b04-4b97-4395-99d1-695465de1140", + "source": [ + "## Run OpenVINO Inference on selected device using Ultralytics API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "In this part of notebook you can select inference device for running model inference to compare results with AUTO." + ] + }, + { + "cell_type": "code", + "id": "41655bec-b006-4376-abbb-d6c8c09e89fb", + "source": [ + "device = device_widget(\"CPU\")\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "f284d5fd-12ba-4c08-afce-d8131ef7ad4d", + "source": [ + "import openvino as ov\n", + "\n", + "core = ov.Core()\n", + "\n", + "ov_model = core.read_model(ov_model_path)\n", + "\n", + "# load model on selected device\n", + "if \"GPU\" in device.value or \"NPU\" in device.value:\n", + " ov_model.reshape({0: [1, 3, 640, 640]})\n", + "ov_config = {}\n", + "if \"GPU\" in device.value:\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "det_compiled_model = core.compile_model(ov_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "id": "12164d21-948a-4ef7-9e1c-b0d41c53cf91", + "source": [ + "ov_yolo_model.predictor.model.ov_compiled_model = det_compiled_model" + ] + }, + { + "cell_type": "code", + "id": "ecb9d396-ab1e-4398-a110-18c08b00ab14", + "source": [ + "res = ov_yolo_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "a4abf758-3a72-4ee0-afde-38076602f1c7", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "992b6ac3-4248-4848-8fd6-f9c1b9fc2051", + "source": [ + "## Optimize model using NNCF Post-training Quantization API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "[NNCF](https://github.com/openvinotoolkit/nncf) provides a suite of advanced algorithms for Neural Networks inference optimization in OpenVINO with minimal accuracy drop.\n", + "We will use 8-bit quantization in post-training mode (without the fine-tuning pipeline) to optimize YOLOv10.\n", + "\n", + "The optimization process contains the following steps:\n", + "\n", + "1. Create a Dataset for quantization.\n", + "2. Run `nncf.quantize` for getting an optimized model.\n", + "3. Serialize OpenVINO IR model, using the `openvino.save_model` function.\n", + "\n", + "Quantization is time and memory consuming process, you can skip this step using checkbox bellow:" + ] + }, + { + "cell_type": "code", + "id": "ed9d9ce5-8df9-4803-ae85-bf0744de914c", + "source": [ + "import ipywidgets as widgets\n", + "\n", + "int8_model_det_path = models_dir / \"int8\" / f\"{model_name}_openvino_model/{model_name}.xml\"\n", + "ov_yolo_int8_model = None\n", + "\n", + "to_quantize = widgets.Checkbox(\n", + " value=True,\n", + " description=\"Quantization\",\n", + " disabled=False,\n", + ")\n", + "\n", + "to_quantize" + ] + }, + { + "cell_type": "code", + "id": "52287a23-b5ef-4c34-873a-32b398d26d1a", + "source": [ + "# Fetch skip_kernel_extension module\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/skip_kernel_extension.py\",\n", + ")\n", + "open(\"skip_kernel_extension.py\", \"w\").write(r.text)\n", + "\n", + "%load_ext skip_kernel_extension" + ] + }, + { + "cell_type": "markdown", + "id": "89ba64af-aa73-4ed9-a2f9-527bd9ae8221", + "source": [ + "### Prepare Quantization Dataset\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "For starting quantization, we need to prepare dataset. We will use validation subset from [MS COCO dataset](https://cocodataset.org/) for model quantization and Ultralytics validation data loader for preparing input data." + ] + }, + { + "cell_type": "code", + "id": "5e10435e-9cd6-4f29-a865-721d6209314d", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "from zipfile import ZipFile\n", + "\n", + "from ultralytics.data.utils import DATASETS_DIR\n", + "\n", + "if not int8_model_det_path.exists():\n", + "\n", + " DATA_URL = \"http://images.cocodataset.org/zips/val2017.zip\"\n", + " LABELS_URL = \"https://github.com/ultralytics/yolov5/releases/download/v1.0/coco2017labels-segments.zip\"\n", + " CFG_URL = \"https://raw.githubusercontent.com/ultralytics/ultralytics/v8.1.0/ultralytics/cfg/datasets/coco.yaml\"\n", + "\n", + " OUT_DIR = DATASETS_DIR\n", + "\n", + " DATA_PATH = OUT_DIR / \"val2017.zip\"\n", + " LABELS_PATH = OUT_DIR / \"coco2017labels-segments.zip\"\n", + " CFG_PATH = OUT_DIR / \"coco.yaml\"\n", + "\n", + " download_file(DATA_URL, DATA_PATH.name, DATA_PATH.parent)\n", + " download_file(LABELS_URL, LABELS_PATH.name, LABELS_PATH.parent)\n", + " download_file(CFG_URL, CFG_PATH.name, CFG_PATH.parent)\n", + "\n", + " if not (OUT_DIR / \"coco/labels\").exists():\n", + " with ZipFile(LABELS_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR)\n", + " with ZipFile(DATA_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR / \"coco/images\")" + ] + }, + { + "cell_type": "code", + "id": "b0a14818-21fd-4c71-8e58-2297dfafaadd", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "from ultralytics.utils import DEFAULT_CFG\n", + "from ultralytics.cfg import get_cfg\n", + "from ultralytics.data.converter import coco80_to_coco91_class\n", + "from ultralytics.data.utils import check_det_dataset\n", + "\n", + "if not int8_model_det_path.exists():\n", + " args = get_cfg(cfg=DEFAULT_CFG)\n", + " args.data = str(CFG_PATH)\n", + " det_validator = ov_yolo_model.task_map[ov_yolo_model.task][\"validator\"](args=args)\n", + "\n", + " det_validator.data = check_det_dataset(args.data)\n", + " det_validator.stride = 32\n", + " det_data_loader = det_validator.get_dataloader(OUT_DIR / \"coco\", 1)" + ] + }, + { + "cell_type": "markdown", + "id": "83fa49f4-e40c-48c7-82a6-8ef56e20b343", + "source": [ + "NNCF provides `nncf.Dataset` wrapper for using native framework dataloaders in quantization pipeline. Additionally, we specify transform function that will be responsible for preparing input data in model expected format." + ] + }, + { + "cell_type": "code", + "id": "253f0e0f-131e-43b9-b1be-639d4b3d1fe5", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "import nncf\n", + "from typing import Dict\n", + "\n", + "\n", + "def transform_fn(data_item:Dict):\n", + " \"\"\"\n", + " Quantization transform function. Extracts and preprocess input data from dataloader item for quantization.\n", + " Parameters:\n", + " data_item: Dict with data item produced by DataLoader during iteration\n", + " Returns:\n", + " input_tensor: Input data for quantization\n", + " \"\"\"\n", + " input_tensor = det_validator.preprocess(data_item)['img'].numpy()\n", + " return input_tensor\n", + "\n", + "if not int8_model_det_path.exists():\n", + " quantization_dataset = nncf.Dataset(det_data_loader, transform_fn)" + ] + }, + { + "cell_type": "markdown", + "id": "ff8a6372-2097-4308-a4e5-e4651242a8df", + "source": [ + "### Quantize and Save INT8 model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The `nncf.quantize` function provides an interface for model quantization. It requires an instance of the OpenVINO Model and quantization dataset. \n", + "Optionally, some additional parameters for the configuration quantization process (number of samples for quantization, preset, ignored scope, etc.) can be provided. YOLOv10 model contains non-ReLU activation functions, which require asymmetric quantization of activations. To achieve a better result, we will use a `mixed` quantization preset. It provides symmetric quantization of weights and asymmetric quantization of activations.\n", + "\n", + ">**Note**: Model post-training quantization is time-consuming process. Be patient, it can take several minutes depending on your hardware." + ] + }, + { + "cell_type": "code", + "id": "4c12ae38-239e-4fe1-8296-47567f98538c", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "import shutil\n", + "\n", + "if not int8_model_det_path.exists():\n", + " quantized_det_model = nncf.quantize(\n", + " ov_model,\n", + " quantization_dataset,\n", + " preset=nncf.QuantizationPreset.MIXED,\n", + " )\n", + "\n", + " ov.save_model(quantized_det_model, int8_model_det_path)\n", + " shutil.copy(ov_model_path.parent / \"metadata.yaml\", int8_model_det_path.parent / \"metadata.yaml\")" + ] + }, + { + "cell_type": "markdown", + "id": "41b8a3eb-adb8-40cf-8602-311ff8d75a76", + "source": [ + "## Run Optimized Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The way of usage INT8 quantized model is the same like for model before quantization. Let's check inference result of quantized model on single image" + ] + }, + { + "cell_type": "markdown", + "id": "f438df41-d614-4cca-a746-a173d92dddf5", + "source": [ + "### Run Optimized Model on AUTO device\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "af61303b-16bd-4206-b926-2c28b3a859a4", + "source": [ + "%%skip not $to_quantize.value\n", + "ov_yolo_int8_model = YOLOv10(int8_model_det_path.parent, task=\"detect\")" + ] + }, + { + "cell_type": "code", + "id": "db13b8dd-49a1-4046-8a1a-491eee095a1b", + "source": [ + "%%skip not $to_quantize.value\n", + "res = ov_yolo_int8_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "0423ba31-f0ee-4bfc-a18a-f7b870a9683d", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "4b4f7118-e046-4f87-b403-87053f2b0ac3", + "source": [ + "### Run Optimized Model Inference on selected device\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "142f22ae-6a8d-4578-b97f-4d9c77123418", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "2f43767a-1994-4202-a411-4738c92223da", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "ov_config = {}\n", + "if \"GPU\" in device.value or \"NPU\" in device.value:\n", + " ov_model.reshape({0: [1, 3, 640, 640]})\n", + "ov_config = {}\n", + "if \"GPU\" in device.value:\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "\n", + "quantized_det_model = core.read_model(int8_model_det_path)\n", + "quantized_det_compiled_model = core.compile_model(quantized_det_model, device.value, ov_config)\n", + "\n", + "ov_yolo_int8_model.predictor.model.ov_compiled_model = quantized_det_compiled_model\n", + "\n", + "res = ov_yolo_int8_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "51d64c7b-7595-41ca-87dd-4c85308f84d6", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "c7b70f75-a706-4f24-a487-88a3ad645dd5", + "source": [ + "## Compare the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "markdown", + "id": "d6561246-a9e0-4cdf-8207-7e2f919d8582", + "source": [ + "### Model size\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "43d84d15-75be-4430-a58b-61cfd8e08dd0", + "source": [ + "ov_model_weights = ov_model_path.with_suffix(\".bin\")\n", + "print(f\"Size of FP16 model is {ov_model_weights.stat().st_size / 1024 / 1024:.2f} MB\")\n", + "if int8_model_det_path.exists():\n", + " ov_int8_weights = int8_model_det_path.with_suffix(\".bin\")\n", + " print(f\"Size of model with INT8 compressed weights is {ov_int8_weights.stat().st_size / 1024 / 1024:.2f} MB\")\n", + " print(f\"Compression rate for INT8 model: {ov_model_weights.stat().st_size / ov_int8_weights.stat().st_size:.3f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f72b3d84-91f3-493c-8f4e-46598ffbd6d1", + "source": [ + "### Performance\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "### FP16 model performance\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "909ea508-e007-4313-ab0f-bfa0e3906176", + "source": [ + "!benchmark_app -m $ov_model_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "markdown", + "id": "9852a8ef-4c45-43dd-a9ef-53ca6f13c99e", + "source": [ + "### Int8 model performance\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "8254626d-6c4f-4ca3-936f-a7c47e402e03", + "source": [ + "if int8_model_det_path.exists():\n", + " !benchmark_app -m $int8_model_det_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "markdown", + "id": "12d35bc1-8101-45ea-8bd7-53720b98f5e5", + "source": [ + "## Live demo\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The following code runs model inference on a video:" + ] + }, + { + "cell_type": "code", + "id": "246bd379-b1d5-4eb2-928f-97c7c2455a92", + "source": [ + "import collections\n", + "import time\n", + "from IPython import display\n", + "import cv2\n", + "import numpy as np\n", + "\n", + "\n", + "# Main processing function to run object detection.\n", + "def run_object_detection(\n", + " source=0,\n", + " flip=False,\n", + " use_popup=False,\n", + " skip_first_frames=0,\n", + " det_model=ov_yolo_int8_model,\n", + " device=device.value,\n", + "):\n", + " player = None\n", + " try:\n", + " # Create a video player to play with target fps.\n", + " player = VideoPlayer(source=source, flip=flip, fps=30, skip_first_frames=skip_first_frames)\n", + " # Start capturing.\n", + " player.start()\n", + " if use_popup:\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(winname=title, flags=cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_AUTOSIZE)\n", + "\n", + " processing_times = collections.deque()\n", + " while True:\n", + " # Grab the frame.\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + " # If the frame is larger than full HD, reduce size to improve the performance.\n", + " scale = 1280 / max(frame.shape)\n", + " if scale < 1:\n", + " frame = cv2.resize(\n", + " src=frame,\n", + " dsize=None,\n", + " fx=scale,\n", + " fy=scale,\n", + " interpolation=cv2.INTER_AREA,\n", + " )\n", + " # Get the results.\n", + " input_image = np.array(frame)\n", + "\n", + " start_time = time.time()\n", + " detections = det_model(input_image, iou=0.45, conf=0.2, verbose=False)\n", + " stop_time = time.time()\n", + " frame = detections[0].plot()\n", + "\n", + " processing_times.append(stop_time - start_time)\n", + " # Use processing times from last 200 frames.\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " _, f_width = frame.shape[:2]\n", + " # Mean processing time [ms].\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + " cv2.putText(\n", + " img=frame,\n", + " text=f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " org=(20, 40),\n", + " fontFace=cv2.FONT_HERSHEY_COMPLEX,\n", + " fontScale=f_width / 1000,\n", + " color=(0, 0, 255),\n", + " thickness=1,\n", + " lineType=cv2.LINE_AA,\n", + " )\n", + " # Use this workaround if there is flickering.\n", + " if use_popup:\n", + " cv2.imshow(winname=title, mat=frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # Encode numpy array to jpg.\n", + " _, encoded_img = cv2.imencode(ext=\".jpg\", img=frame, params=[cv2.IMWRITE_JPEG_QUALITY, 100])\n", + " # Create an IPython image.\n", + " i = display.Image(data=encoded_img)\n", + " # Display the image in this notebook.\n", + " display.clear_output(wait=True)\n", + " display.display(i)\n", + " # ctrl-c\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " # any different error\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " if player is not None:\n", + " # Stop capturing.\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()" + ] + }, + { + "cell_type": "code", + "id": "8db930f4-fc41-4635-9248-deab2b6cfcb8", + "source": [ + "use_int8 = widgets.Checkbox(\n", + " value=ov_yolo_int8_model is not None,\n", + " description=\"Use int8 model\",\n", + " disabled=ov_yolo_int8_model is None,\n", + ")\n", + "\n", + "use_int8" + ] + }, + { + "cell_type": "code", + "id": "59e37135-1bdc-40ff-8789-b5b4491cab84", + "source": [ + "WEBCAM_INFERENCE = False\n", + "\n", + "if WEBCAM_INFERENCE:\n", + " VIDEO_SOURCE = 0 # Webcam\n", + "else:\n", + " download_file(\n", + " \"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/video/people.mp4\",\n", + " directory=\"data\",\n", + " )\n", + " VIDEO_SOURCE = \"data/people.mp4\"" + ] + }, + { + "cell_type": "code", + "id": "88c6f47c-1c47-451d-89ba-0a29760f5ec4", + "source": [ + "run_object_detection(\n", + " det_model=ov_yolo_model if not use_int8.value else ov_yolo_int8_model,\n", + " source=VIDEO_SOURCE,\n", + " flip=True,\n", + " use_popup=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9f612d80-68e2-45f8-aa62-9f3a05ca9de8", + "source": [ + "### Gradio Interactive Demo\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "2e5a6249", + "source": [ + "def yolov10_inference(image, int8, conf_threshold, iou_threshold):\n", + " model = ov_yolo_model if not int8 else ov_yolo_int8_model\n", + " results = model(source=image, iou=iou_threshold, conf=conf_threshold, verbose=False)[0]\n", + " annotated_image = Image.fromarray(results.plot())\n", + "\n", + " return annotated_image" + ] + }, + { + "cell_type": "code", + "id": "ffdbe248-7086-4804-b4b9-c3dd454bf80c", + "source": [ + "if not Path(\"gradio_helper.py\").exists():\n", + " r = requests.get(url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/notebooks/yolov10-optimization/gradio_helper.py\")\n", + " open(\"gradio_helper.py\", \"w\").write(r.text)\n", + "\n", + "from gradio_helper import make_demo\n", + "\n", + "demo = make_demo(fn=yolov10_inference, quantized=ov_yolo_int8_model is not None)\n", + "\n", + "try:\n", + " demo.launch(debug=True)\n", + "except Exception:\n", + " demo.launch(debug=True, share=True)\n", + "# If you are launching remotely, specify server_name and server_port\n", + "# EXAMPLE: `demo.launch(server_name='your server name', server_port='server port in int')`\n", + "# To learn more please refer to the Gradio docs: https://gradio.app/docs/" + ] + }, + { + "cell_type": "code", + "id": "4b123c5e", + "source": [ + "# please uncomment and run this cell for stopping gradio interface\n", + "# demo.close()" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + { modified: 24, original: 24 }, + { modified: 25, original: 25 }, + { modified: 26, original: 26 }, + { modified: 27, original: 27 }, + { modified: 28, original: 28 }, + { modified: 29, original: 29 }, + { modified: 30, original: 30 }, + { modified: 31, original: 31 }, + { modified: 32, original: 32 }, + { modified: 33, original: 33 }, + { modified: 34, original: 34 }, + { modified: 35, original: 35 }, + { modified: 36, original: 36 }, + { modified: 37, original: 37 }, + { modified: 38, original: 38 }, + { modified: 39, original: 39 }, + { modified: 40, original: 40 }, + { modified: 41, original: 41 }, + { modified: 42, original: 42 }, + { modified: 43, original: 43 }, + { modified: 44, original: 44 }, + { modified: 45, original: 45 }, + { modified: 46, original: 46 }, + { modified: 47, original: 47 }, + { modified: 48, original: 48 }, + { modified: 49, original: 49 }, + { modified: 50, original: 50 }, + { modified: 51, original: 51 }, + { modified: 52, original: 52 }, + { modified: 53, original: -1 }, + { modified: 54, original: -1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 52, originalLength: 1, modifiedStart: 52, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 53, originalLength: 0, modifiedStart: 53, modifiedLength: 2 } satisfies IDiffChange, + ]); + + }); + test('Misalignment due to 2 deleted cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "metadata": { + "id": "F0Wut2G5kz2Z" + }, + "source": [ + "[![Roboflow Notebooks](https://ik.imagekit.io/roboflow/notebooks/template/bannertest2-2.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672932710194)](https://github.com/roboflow/notebooks)\n", + "\n", + "# Fast Segment Anything Model (FastSAM)\n", + "\n", + "---\n", + "\n", + "[![GitHub](https://badges.aleen42.com/src/github.svg)](https://github.com/CASIA-IVA-Lab/FastSAM)\n", + "[![arXiv](https://img.shields.io/badge/arXiv-2306.12156-b31b1b.svg)](https://arxiv.org/pdf/2306.12156.pdf)\n", + "[![YouTube](https://badges.aleen42.com/src/youtube.svg)](https://youtu.be/yHNPyqazYYU)\n", + "[![Roboflow](https://raw.githubusercontent.com/roboflow-ai/notebooks/main/assets/badges/roboflow-blogpost.svg)](https://blog.roboflow.com/how-to-use-fastsam)\n", + "\n", + "The Fast Segment Anything Model (FastSAM) is a CNN Segment Anything Model trained by only 2% of the SA-1B dataset published by SAM authors.\n", + "\n", + "![fast-segment-anything-model-figure-1](https://media.roboflow.com/notebooks/examples/fast-sam-figure-1.png)\n", + "\n", + "The FastSAM authors claim it achieves comparable performance to the SAM method at 50 times the speed.\n", + "\n", + "![fast-segment-anything-model-figure-2](https://media.roboflow.com/notebooks/examples/fast-sam-figure-2.png)\n", + "\n", + "## Complementary Materials Covering SAM\n", + "\n", + "---\n", + "\n", + "[![GitHub](https://badges.aleen42.com/src/github.svg)](https://github.com/facebookresearch/segment-anything)\n", + "[![arXiv](https://img.shields.io/badge/arXiv-2304.02643-b31b1b.svg)](https://arxiv.org/abs/2304.02643)\n", + "[![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-segment-anything-with-sam.ipynb)\n", + "[![YouTube](https://badges.aleen42.com/src/youtube.svg)](https://youtu.be/D-D6ZmadzPE)\n", + "[![Roboflow](https://raw.githubusercontent.com/roboflow-ai/notebooks/main/assets/badges/roboflow-blogpost.svg)](https://blog.roboflow.com/how-to-use-segment-anything-model-sam)\n", + "\n", + "## Steps in this Tutorial\n", + "\n", + "In this tutorial, we are going to cover:\n", + "\n", + "- Before you start\n", + "- Install FastSAM, SAM, and other dependencies\n", + "- Download FastSAM and SAM weights\n", + "- Download example data\n", + "- Imports\n", + "- Load FastSAM\n", + "- FastSAM inference\n", + "- FastSAM box prompt inference\n", + "- FastSAM point prompt inference\n", + "- FastSAM text prompt inference\n", + "- SAM vs FastSAM\n", + "- Roboflow benchmark dataset\n", + "\n", + "## 🔥 Let's begin!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W4u6KjiVdNZM" + }, + "source": [ + "## Before you start\n", + "\n", + "Let's make sure that we have access to GPU. We can use `nvidia-smi` command to do that. In case of any problems navigate to `Edit` -> `Notebook settings` -> `Hardware accelerator`, set it to `GPU`, and then click `Save`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Frcrk09FhJeV", + "outputId": "683a378e-4a8f-4239-901b-037279e92e2b" + }, + "outputs": [], + "source": [ + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PUACTM2zdmJI" + }, + "source": [ + "**NOTE:** To make it easier for us to manage datasets, images and models we create a `HOME` constant." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "PRtHQpTNdnm7", + "outputId": "2824c70d-dcf9-4006-b32d-adc6797fd708" + }, + "outputs": [], + "source": [ + "import os\n", + "HOME = os.getcwd()\n", + "print(\"HOME:\", HOME)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YN3DPGZSn57p" + }, + "source": [ + "## Install FastSAM, SAM, and other dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Vr3kcq1KfFir", + "outputId": "234e5336-ba28-4114-d9e7-efd7f2752ca8" + }, + "outputs": [], + "source": [ + "%cd {HOME}\n", + "\n", + "# install FastSAM\n", + "!git clone https://github.com/CASIA-IVA-Lab/FastSAM.git\n", + "!pip -q install -r FastSAM/requirements.txt\n", + "# install CLIP\n", + "!pip -q install git+https://github.com/openai/CLIP.git\n", + "# install SAM\n", + "!pip -q install git+https://github.com/facebookresearch/segment-anything.git\n", + "# install other dependencies\n", + "!pip -q install roboflow supervision jupyter_bbox_widget" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BMmgSfDWfFis" + }, + "source": [ + "## Download FastSAM and SAM weights" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "ADpOBElSfFis", + "outputId": "b3d89d4c-e2ad-4f5e-a910-b52695263151" + }, + "outputs": [], + "source": [ + "!mkdir -p {HOME}/weights\n", + "!wget -P {HOME}/weights -q https://huggingface.co/spaces/An-619/FastSAM/resolve/main/weights/FastSAM.pt\n", + "!wget -P {HOME}/weights -q https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth\n", + "!ls -lh {HOME}/weights" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "W6w1ck4z72sn" + }, + "outputs": [], + "source": [ + "FAST_SAM_CHECKPOINT_PATH = f\"{HOME}/weights/FastSAM.pt\"\n", + "SAM_SAM_CHECKPOINT_PATH = f\"{HOME}/weights/sam_vit_h_4b8939.pth\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "t1ef2h1R9WMb" + }, + "source": [ + "## Download example data\n", + "\n", + "**NONE:** Let's download few example images. Feel free to use your images or videos." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "CL4gVrnl9dmR", + "outputId": "db8f78d2-eaf4-41de-9d1f-e5fd6b588a87" + }, + "outputs": [], + "source": [ + "!mkdir -p {HOME}/data\n", + "!wget -P {HOME}/data -q https://media.roboflow.com/notebooks/examples/dog.jpeg\n", + "!wget -P {HOME}/data -q https://media.roboflow.com/notebooks/examples/robot.jpeg\n", + "!ls -lh {HOME}/data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J_a_icikYiVN" + }, + "source": [ + "## Imports\n", + "\n", + "**NOTE:** `FastSAM` code is not distributed via `pip` not it is packaged. Make sure to run code below from `{HOME}/FastSAM` directory. ⚠️" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "u_NVTXOwYppS", + "outputId": "b86184de-5827-4b9c-eb5c-959539abe699" + }, + "outputs": [], + "source": [ + "%cd {HOME}/FastSAM\n", + "\n", + "import os\n", + "import cv2\n", + "import torch\n", + "import roboflow\n", + "import base64\n", + "\n", + "import supervision as sv\n", + "import numpy as np\n", + "\n", + "from roboflow import Roboflow\n", + "from fastsam import FastSAM, FastSAMPrompt\n", + "from segment_anything import sam_model_registry, SamAutomaticMaskGenerator, SamPredictor" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a_Iizsy07pb7" + }, + "source": [ + "## Load FastSAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ao8tVDVq7ebG" + }, + "outputs": [], + "source": [ + "DEVICE = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n", + "print(f\"DEVICE = {DEVICE}\")\n", + "fast_sam = FastSAM(FAST_SAM_CHECKPOINT_PATH)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SvsRFtw_--Nn" + }, + "source": [ + "## FastSAM inference\n", + "\n", + "* `retina_masks=True` determines whether the model uses retina masks for generating segmentation masks.\n", + "* `imgsz=1024` sets the input image size to 1024x1024 pixels for processing by the model.\n", + "* `conf=0.4` sets the minimum confidence threshold for object detection.\n", + "* `iou=0.9` sets the minimum intersection over union threshold for non-maximum suppression to filter out duplicate detections." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "L49P5e-0Iaea" + }, + "outputs": [], + "source": [ + "IMAGE_PATH = f\"{HOME}/data/dog.jpeg\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os.makedirs(f\"{HOME}/output\", exist_ok=True)\n", + "output_image_path = f\"{HOME}/output/output-{os.path.basename(IMAGE_PATH)}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "KV4XzCg1_fAS", + "outputId": "1df75d90-cf73-4d78-feed-01911a81f6e4" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.4,\n", + " iou=0.9)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()\n", + "prompt_process.plot(annotations=masks, output=f\"{HOME}/output\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uPrxqbaBOBMw" + }, + "source": [ + "**NOTE:** `prompt_process.everything_prompt` returns `torch.Tensor` when DEVICE = 'cuda:0'. `prompt_process.everything_prompt` returns `numpy.ndarray` when DEVICE = 'cpu'." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Convert masks to boolean (True/False)\n", + "def masks_to_bool(masks):\n", + " if type(masks) == np.ndarray:\n", + " masks = masks.astype(bool)\n", + " else:\n", + " masks = masks.cpu().numpy().astype(bool)\n", + " return masks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PUJkojNiKBQa" + }, + "outputs": [], + "source": [ + "def annotate_image(image_path: str, masks: np.ndarray) -> np.ndarray:\n", + " image = cv2.imread(image_path)\n", + "\n", + " xyxy = sv.mask_to_xyxy(masks=masks)\n", + " detections = sv.Detections(xyxy=xyxy, mask=masks)\n", + "\n", + " mask_annotator = sv.MaskAnnotator(color_lookup = sv.ColorLookup.INDEX)\n", + " return mask_annotator.annotate(scene=image.copy(), detections=detections)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "wwtmiH6pLA8N", + "outputId": "17948850-f051-4a1f-b2b8-d01c2a1f4f20" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jYkdczKoRUVB" + }, + "source": [ + "**NOTE:** The quality of the generated masks is quite poor. Let's see if we can improve it by manipulating the parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "dDFGi6EhO2ul", + "outputId": "f5a03357-eb37-446e-dce7-0659e4c2a368" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "OiBPjQW-O59c", + "outputId": "afb011fc-933e-4d69-b4dd-e5e2713f407b" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GoymBQ16KJbv" + }, + "source": [ + "## FastSAM box prompt inference" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "w1_T5SXpMNku" + }, + "source": [ + "### Draw Box" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "04-1G1WrMJUm" + }, + "outputs": [], + "source": [ + "# helper function that loads an image before adding it to the widget\n", + "\n", + "def encode_image(filepath):\n", + " with open(filepath, 'rb') as f:\n", + " image_bytes = f.read()\n", + " encoded = str(base64.b64encode(image_bytes), 'utf-8')\n", + " return \"data:image/jpg;base64,\"+encoded" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xrxd-Ug9MSWG" + }, + "source": [ + "**NOTE:** Execute cell below and use your mouse to draw bounding box on the image 👇" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "a747e095e21448f29d8697fb6b1a7a74", + "6401ca8c2f574ba5ab62f0f06b9e0d06" + ] + }, + "id": "nLA7xmDIMTfx", + "outputId": "06b51195-d54f-44ed-9236-258e94d624c5" + }, + "outputs": [], + "source": [ + "IS_COLAB = True\n", + "\n", + "if IS_COLAB:\n", + " from google.colab import output\n", + " output.enable_custom_widget_manager()\n", + "\n", + "from jupyter_bbox_widget import BBoxWidget\n", + "\n", + "widget = BBoxWidget()\n", + "widget.image = encode_image(IMAGE_PATH)\n", + "widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "td6tYT4INOyD", + "outputId": "73a4e886-dcb8-447c-d3f7-1b3a6496e44d" + }, + "outputs": [], + "source": [ + "widget.bboxes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XfUvm9bwNUAe" + }, + "source": [ + "### Generate mask with FastSAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bI6kPDSROBI-" + }, + "outputs": [], + "source": [ + "# default_box is going to be used if you will not draw any box on image above\n", + "default_box = {'x': 68, 'y': 247, 'width': 555, 'height': 678, 'label': ''}\n", + "\n", + "box = widget.bboxes[0] if widget.bboxes else default_box\n", + "box = [\n", + " box['x'],\n", + " box['y'],\n", + " box['x'] + box['width'],\n", + " box['y'] + box['height']\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LF879hDqO5iB", + "outputId": "4e7498c4-4007-4614-d46c-0e35608a04a2" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.box_prompt(bbox=box)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yt6i2sNJPEdk" + }, + "source": [ + "**NOTE:** `prompt_process.box_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "EJThTct_PA68", + "outputId": "f6eddd24-f603-4702-8325-6cff7925fe0f" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vR-THJthhWH5" + }, + "source": [ + "## FastSAM point prompt inference" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xyfpOPPwhpMy" + }, + "source": [ + "### Select point" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OC7d3pZ3hop9" + }, + "outputs": [], + "source": [ + "point = [405, 505]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rE5tV4e3h6nG" + }, + "source": [ + "### Generate mask with FastSAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "31FIby6Zh-Wa", + "outputId": "a12c4d1c-837b-4b1b-9240-664797442413" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.point_prompt(points=[point], pointlabel=[1])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d7tpVo8CiM3s" + }, + "source": [ + "**NOTE:** `prompt_process.point_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "lMVpGxShiPky", + "outputId": "224d84ee-e664-400b-c0df-49507d229f62" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HOC7v6ATHEr6" + }, + "source": [ + "## FastSAM text prompt inference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "4CzLLtXOHeGk", + "outputId": "cecc50d8-e2be-4b33-850a-a2442e3e30b6" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.text_prompt(text='cap')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KYW79zD0OKsv" + }, + "source": [ + "**NOTE:** `prompt_process.text_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "zODY5xU1LMCb", + "outputId": "a01540a7-deb3-4555-916c-2ac59928e0f3" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XFOUWUMZEk7m" + }, + "source": [ + "## SAM vs FastSAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7PKBZ7Hhahhw" + }, + "outputs": [], + "source": [ + "IMAGE_PATH = f\"{HOME}/data/robot.jpeg\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c48hRY7dVJeL" + }, + "source": [ + "### Load SAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PdKpFCCmaETG" + }, + "outputs": [], + "source": [ + "MODEL_TYPE = \"vit_h\"\n", + "\n", + "sam = sam_model_registry[MODEL_TYPE](checkpoint=SAM_SAM_CHECKPOINT_PATH).to(device=DEVICE)\n", + "mask_generator = SamAutomaticMaskGenerator(sam)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V5bPGvtBaewO" + }, + "source": [ + "### SAM inference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0HgWsk1yalow" + }, + "outputs": [], + "source": [ + "image_bgr = cv2.imread(IMAGE_PATH)\n", + "image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)\n", + "\n", + "sam_result = mask_generator.generate(image_rgb)\n", + "sam_detections = sv.Detections.from_sam(sam_result=sam_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "t03RrSzMasN6" + }, + "source": [ + "### FastSAM inference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "xuFGLw8MavQI", + "outputId": "b0118973-1e29-419d-b7c0-2933b97d7ce5" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()\n", + "masks = masks_to_bool(masks)\n", + "xyxy = sv.mask_to_xyxy(masks=masks)\n", + "fast_sam_detections = sv.Detections(xyxy=xyxy, mask=masks)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IYn8xKySbBnc" + }, + "source": [ + "### FastSAM vs. SAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 321 + }, + "id": "c2Qj7sLjbDvo", + "outputId": "e85e3ed3-8b90-409d-cf95-877a6a9b2034" + }, + "outputs": [], + "source": [ + "mask_annotator = sv.MaskAnnotator(color_lookup = sv.ColorLookup.INDEX)\n", + "\n", + "sam_result = mask_annotator.annotate(scene=image_bgr.copy(), detections=sam_detections)\n", + "fast_sam_result = mask_annotator.annotate(scene=image_bgr.copy(), detections=fast_sam_detections)\n", + "\n", + "sv.plot_images_grid(\n", + " images=[sam_result, fast_sam_result],\n", + " grid_size=(1, 2),\n", + " titles=['SAM', 'FastSAM']\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MfU4NQiGWQMC" + }, + "source": [ + "**NOTE:** There is a lot going on in our test image. Let's plot our masks against a blank background." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 321 + }, + "id": "PIrywlF5QRPz", + "outputId": "8e7c84ae-7869-4523-847a-0678fd1aea53" + }, + "outputs": [], + "source": [ + "mask_annotator = sv.MaskAnnotator(color_lookup = sv.ColorLookup.INDEX)\n", + "\n", + "sam_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=sam_detections)\n", + "fast_sam_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=fast_sam_detections)\n", + "\n", + "sv.plot_images_grid(\n", + " images=[sam_result, fast_sam_result],\n", + " grid_size=(1, 2),\n", + " titles=['SAM', 'FastSAM']\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "51Fs3sDGQmIt" + }, + "source": [ + "### Mask diff\n", + "\n", + "**NOTE:** Let's try to understand which masks generated by SAM were not generated by FastSAM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "O5bIUAhYQrqZ" + }, + "outputs": [], + "source": [ + "def compute_iou(mask1, mask2):\n", + " intersection = np.logical_and(mask1, mask2)\n", + " union = np.logical_or(mask1, mask2)\n", + " return np.sum(intersection) / np.sum(union)\n", + "\n", + "def filter_masks(mask_array1, mask_array2, threshold):\n", + " return np.array([mask1 for mask1 in mask_array1 if max(compute_iou(mask1, mask2) for mask2 in mask_array2) < threshold])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 420 + }, + "id": "1GM4bL9sRbOm", + "outputId": "421fb4f5-7a42-461b-ba1e-7cd46d2d7bd7" + }, + "outputs": [], + "source": [ + "diff_masks = filter_masks(sam_detections.mask, fast_sam_detections.mask, 0.5)\n", + "diff_xyxy = sv.mask_to_xyxy(masks=diff_masks)\n", + "diff_detections = sv.Detections(xyxy=diff_xyxy, mask=diff_masks)\n", + "\n", + "mask_annotator = sv.MaskAnnotator(color_lookup = sv.ColorLookup.INDEX)\n", + "diff_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=diff_detections)\n", + "sv.plot_image(image=diff_result, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0lPZAZZulhF8" + }, + "source": [ + "## Roboflow Benchmark Dataset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LWN6XwPuSZBY", + "outputId": "86b18c0e-f6d6-486b-eee6-d004f13f531e" + }, + "outputs": [], + "source": [ + "%cd {HOME}\n", + "\n", + "roboflow.login()\n", + "rf = Roboflow()\n", + "\n", + "project = rf.workspace(\"roboticfish\").project(\"underwater_object_detection\")\n", + "dataset = project.version(10).download(\"yolov8\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "T6-PaxWXTZ69" + }, + "outputs": [], + "source": [ + "IMAGE_PATH = os.path.join(dataset.location, 'train', 'images', 'IMG_2311_jpeg_jpg.rf.09ae6820eaff21dc838b1f9b6b20342b.jpg')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "p-gY5jf1Tife", + "outputId": "630d35d8-26f4-4342-8d82-c7b61be26fe8" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.text_prompt(text='Penguin')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "3tBIH0yubHsW", + "outputId": "fc4d0a6a-53c1-4a2b-91f0-75c8f78e10fd" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gP9gEw9Tp8GJ" + }, + "source": [ + "## 🏆 Congratulations\n", + "\n", + "### Learning Resources\n", + "\n", + "Roboflow has produced many resources that you may find interesting as you advance your knowledge of computer vision:\n", + "\n", + "- [Roboflow Notebooks](https://github.com/roboflow/notebooks): A repository of over 20 notebooks that walk through how to train custom models with a range of model types, from YOLOv7 to SegFormer.\n", + "- [Roboflow YouTube](https://www.youtube.com/c/Roboflow): Our library of videos featuring deep dives into the latest in computer vision, detailed tutorials that accompany our notebooks, and more.\n", + "- [Roboflow Discuss](https://discuss.roboflow.com/): Have a question about how to do something on Roboflow? Ask your question on our discussion forum.\n", + "- [Roboflow Models](https://roboflow.com): Learn about state-of-the-art models and their performance. Find links and tutorials to guide your learning.\n", + "\n", + "### Convert data formats\n", + "\n", + "Roboflow provides free utilities to convert data between dozens of popular computer vision formats. Check out [Roboflow Formats](https://roboflow.com/formats) to find tutorials on how to convert data between formats in a few clicks.\n", + "\n", + "### Connect computer vision to your project logic\n", + "\n", + "[Roboflow Templates](https://roboflow.com/templates) is a public gallery of code snippets that you can use to connect computer vision to your project logic. Code snippets range from sending emails after inference to measuring object distance between detections." + ] + } + ] + .map(fromJupyterCell) + , + [ + { + "cell_type": "markdown", + "metadata": { + "id": "F0Wut2G5kz2Z" + }, + "source": [ + "[![Roboflow Notebooks](https://ik.imagekit.io/roboflow/notebooks/template/bannertest2-2.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672932710194)](https://github.com/roboflow/notebooks)\n", + "\n", + "# Fast Segment Anything Model (FastSAM)\n", + "\n", + "---\n", + "\n", + "[![GitHub](https://badges.aleen42.com/src/github.svg)](https://github.com/CASIA-IVA-Lab/FastSAM)\n", + "[![arXiv](https://img.shields.io/badge/arXiv-2306.12156-b31b1b.svg)](https://arxiv.org/pdf/2306.12156.pdf)\n", + "[![YouTube](https://badges.aleen42.com/src/youtube.svg)](https://youtu.be/yHNPyqazYYU)\n", + "[![Roboflow](https://raw.githubusercontent.com/roboflow-ai/notebooks/main/assets/badges/roboflow-blogpost.svg)](https://blog.roboflow.com/how-to-use-fastsam)\n", + "\n", + "The Fast Segment Anything Model (FastSAM) is a CNN Segment Anything Model trained by only 2% of the SA-1B dataset published by SAM authors.\n", + "\n", + "![fast-segment-anything-model-figure-1](https://media.roboflow.com/notebooks/examples/fast-sam-figure-1.png)\n", + "\n", + "The FastSAM authors claim it achieves comparable performance to the SAM method at 50 times the speed.\n", + "\n", + "![fast-segment-anything-model-figure-2](https://media.roboflow.com/notebooks/examples/fast-sam-figure-2.png)\n", + "\n", + "## Complementary Materials Covering SAM\n", + "\n", + "---\n", + "\n", + "[![GitHub](https://badges.aleen42.com/src/github.svg)](https://github.com/facebookresearch/segment-anything)\n", + "[![arXiv](https://img.shields.io/badge/arXiv-2304.02643-b31b1b.svg)](https://arxiv.org/abs/2304.02643)\n", + "[![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-segment-anything-with-sam.ipynb)\n", + "[![YouTube](https://badges.aleen42.com/src/youtube.svg)](https://youtu.be/D-D6ZmadzPE)\n", + "[![Roboflow](https://raw.githubusercontent.com/roboflow-ai/notebooks/main/assets/badges/roboflow-blogpost.svg)](https://blog.roboflow.com/how-to-use-segment-anything-model-sam)\n", + "\n", + "## Steps in this Tutorial\n", + "\n", + "In this tutorial, we are going to cover:\n", + "\n", + "- Before you start\n", + "- Install FastSAM, SAM, and other dependencies\n", + "- Download FastSAM and SAM weights\n", + "- Download example data\n", + "- Imports\n", + "- Load FastSAM\n", + "- FastSAM inference\n", + "- FastSAM box prompt inference\n", + "- FastSAM point prompt inference\n", + "- FastSAM text prompt inference\n", + "- SAM vs FastSAM\n", + "- Roboflow benchmark dataset\n", + "\n", + "## 🔥 Let's begin!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W4u6KjiVdNZM" + }, + "source": [ + "## Before you start\n", + "\n", + "Let's make sure that we have access to GPU. We can use `nvidia-smi` command to do that. In case of any problems navigate to `Edit` -> `Notebook settings` -> `Hardware accelerator`, set it to `GPU`, and then click `Save`." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Frcrk09FhJeV", + "outputId": "683a378e-4a8f-4239-901b-037279e92e2b" + }, + "source": [ + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PUACTM2zdmJI" + }, + "source": [ + "**NOTE:** To make it easier for us to manage datasets, images and models we create a `HOME` constant." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "PRtHQpTNdnm7", + "outputId": "2824c70d-dcf9-4006-b32d-adc6797fd708" + }, + "source": [ + "import os\n", + "HOME = os.getcwd()\n", + "print(\"HOME:\", HOME)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YN3DPGZSn57p" + }, + "source": [ + "## Install FastSAM, SAM, and other dependencies" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Vr3kcq1KfFir", + "outputId": "234e5336-ba28-4114-d9e7-efd7f2752ca8" + }, + "source": [ + "%cd {HOME}\n", + "\n", + "# install FastSAM\n", + "!git clone https://github.com/CASIA-IVA-Lab/FastSAM.git\n", + "!pip -q install -r FastSAM/requirements.txt\n", + "# install CLIP\n", + "!pip -q install git+https://github.com/openai/CLIP.git\n", + "# install SAM\n", + "!pip -q install git+https://github.com/facebookresearch/segment-anything.git\n", + "# install other dependencies\n", + "!pip -q install roboflow supervision jupyter_bbox_widget" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BMmgSfDWfFis" + }, + "source": [ + "## Download FastSAM and SAM weights" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "ADpOBElSfFis", + "outputId": "b3d89d4c-e2ad-4f5e-a910-b52695263151" + }, + "source": [ + "!mkdir -p {HOME}/weights\n", + "!wget -P {HOME}/weights -q https://huggingface.co/spaces/An-619/FastSAM/resolve/main/weights/FastSAM.pt\n", + "!wget -P {HOME}/weights -q https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth\n", + "!ls -lh {HOME}/weights" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "W6w1ck4z72sn" + }, + "source": [ + "FAST_SAM_CHECKPOINT_PATH = f\"{HOME}/weights/FastSAM.pt\"\n", + "SAM_SAM_CHECKPOINT_PATH = f\"{HOME}/weights/sam_vit_h_4b8939.pth\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "t1ef2h1R9WMb" + }, + "source": [ + "## Download example data\n", + "\n", + "**NONE:** Let's download few example images. Feel free to use your images or videos." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "CL4gVrnl9dmR", + "outputId": "db8f78d2-eaf4-41de-9d1f-e5fd6b588a87" + }, + "source": [ + "!mkdir -p {HOME}/data\n", + "!wget -P {HOME}/data -q https://media.roboflow.com/notebooks/examples/dog.jpeg\n", + "!wget -P {HOME}/data -q https://media.roboflow.com/notebooks/examples/robot.jpeg\n", + "!ls -lh {HOME}/data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J_a_icikYiVN" + }, + "source": [ + "## Imports\n", + "\n", + "**NOTE:** `FastSAM` code is not distributed via `pip` not it is packaged. Make sure to run code balow from `{HOME}/FastSAM` directory. ⚠️" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "u_NVTXOwYppS", + "outputId": "b86184de-5827-4b9c-eb5c-959539abe699" + }, + "source": [ + "%cd {HOME}/FastSAM\n", + "\n", + "import os\n", + "import cv2\n", + "import torch\n", + "import roboflow\n", + "import base64\n", + "\n", + "import supervision as sv\n", + "import numpy as np\n", + "\n", + "from roboflow import Roboflow\n", + "from fastsam import FastSAM, FastSAMPrompt\n", + "from segment_anything import sam_model_registry, SamAutomaticMaskGenerator, SamPredictor" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a_Iizsy07pb7" + }, + "source": [ + "## Load FastSAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "ao8tVDVq7ebG" + }, + "source": [ + "DEVICE = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n", + "\n", + "fast_sam = FastSAM(FAST_SAM_CHECKPOINT_PATH)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SvsRFtw_--Nn" + }, + "source": [ + "## FastSAM inference\n", + "\n", + "* `retina_masks=True` determines whether the model uses retina masks for generating segmentation masks.\n", + "* `imgsz=1024` sets the input image size to 1024x1024 pixels for processing by the model.\n", + "* `conf=0.4` sets the minimum confidence threshold for object detection.\n", + "* `iou=0.9` sets the minimum intersection over union threshold for non-maximum suppression to filter out duplicate detections." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "L49P5e-0Iaea" + }, + "source": [ + "IMAGE_PATH = f\"{HOME}/data/dog.jpeg\"" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "KV4XzCg1_fAS", + "outputId": "1df75d90-cf73-4d78-feed-01911a81f6e4" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.4,\n", + " iou=0.9)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()\n", + "prompt_process.plot(annotations=masks, output=f\"{HOME}/output\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uPrxqbaBOBMw" + }, + "source": [ + "**NOTE:** `prompt_process.everything_prompt` returns `torch.Tensor`" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "PUJkojNiKBQa" + }, + "source": [ + "def annotate_image(image_path: str, masks: np.ndarray) -> np.ndarray:\n", + " image = cv2.imread(image_path)\n", + "\n", + " xyxy = sv.mask_to_xyxy(masks=masks)\n", + " detections = sv.Detections(xyxy=xyxy, mask=masks)\n", + "\n", + " mask_annotator = sv.MaskAnnotator()\n", + " return mask_annotator.annotate(scene=image.copy(), detections=detections)" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "wwtmiH6pLA8N", + "outputId": "17948850-f051-4a1f-b2b8-d01c2a1f4f20" + }, + "source": [ + "masks = masks.cpu().numpy().astype(bool)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jYkdczKoRUVB" + }, + "source": [ + "**NOTE:** The quality of the generated masks is quite poor. Let's see if we can improve it by manipulating the parameters." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "dDFGi6EhO2ul", + "outputId": "f5a03357-eb37-446e-dce7-0659e4c2a368" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "OiBPjQW-O59c", + "outputId": "afb011fc-933e-4d69-b4dd-e5e2713f407b" + }, + "source": [ + "masks = masks.cpu().numpy().astype(bool)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GoymBQ16KJbv" + }, + "source": [ + "## FastSAM box prompt inference" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "w1_T5SXpMNku" + }, + "source": [ + "### Draw Box" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "04-1G1WrMJUm" + }, + "source": [ + "# helper function that loads an image before adding it to the widget\n", + "\n", + "def encode_image(filepath):\n", + " with open(filepath, 'rb') as f:\n", + " image_bytes = f.read()\n", + " encoded = str(base64.b64encode(image_bytes), 'utf-8')\n", + " return \"data:image/jpg;base64,\"+encoded" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xrxd-Ug9MSWG" + }, + "source": [ + "**NOTE:** Execute cell below and use your mouse to draw bounding box on the image 👇" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "a747e095e21448f29d8697fb6b1a7a74", + "6401ca8c2f574ba5ab62f0f06b9e0d06" + ] + }, + "id": "nLA7xmDIMTfx", + "outputId": "06b51195-d54f-44ed-9236-258e94d624c5" + }, + "source": [ + "IS_COLAB = True\n", + "\n", + "if IS_COLAB:\n", + " from google.colab import output\n", + " output.enable_custom_widget_manager()\n", + "\n", + "from jupyter_bbox_widget import BBoxWidget\n", + "\n", + "widget = BBoxWidget()\n", + "widget.image = encode_image(IMAGE_PATH)\n", + "widget" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "td6tYT4INOyD", + "outputId": "73a4e886-dcb8-447c-d3f7-1b3a6496e44d" + }, + "source": [ + "widget.bboxes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XfUvm9bwNUAe" + }, + "source": [ + "### Generate mask with FastSAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "bI6kPDSROBI-" + }, + "source": [ + "# default_box is going to be used if you will not draw any box on image above\n", + "default_box = {'x': 68, 'y': 247, 'width': 555, 'height': 678, 'label': ''}\n", + "\n", + "box = widget.bboxes[0] if widget.bboxes else default_box\n", + "box = [\n", + " box['x'],\n", + " box['y'],\n", + " box['x'] + box['width'],\n", + " box['y'] + box['height']\n", + "]" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LF879hDqO5iB", + "outputId": "4e7498c4-4007-4614-d46c-0e35608a04a2" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.box_prompt(bbox=box)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yt6i2sNJPEdk" + }, + "source": [ + "**NOTE:** `prompt_process.box_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "EJThTct_PA68", + "outputId": "f6eddd24-f603-4702-8325-6cff7925fe0f" + }, + "source": [ + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vR-THJthhWH5" + }, + "source": [ + "## FastSAM point prompt inference" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xyfpOPPwhpMy" + }, + "source": [ + "### Select point" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "OC7d3pZ3hop9" + }, + "source": [ + "point = [405, 505]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rE5tV4e3h6nG" + }, + "source": [ + "### Generate mask with FastSAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "31FIby6Zh-Wa", + "outputId": "a12c4d1c-837b-4b1b-9240-664797442413" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.point_prompt(points=[point], pointlabel=[1])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d7tpVo8CiM3s" + }, + "source": [ + "**NOTE:** `prompt_process.point_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "lMVpGxShiPky", + "outputId": "224d84ee-e664-400b-c0df-49507d229f62" + }, + "source": [ + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HOC7v6ATHEr6" + }, + "source": [ + "## FastSAM text prompt inference" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "4CzLLtXOHeGk", + "outputId": "cecc50d8-e2be-4b33-850a-a2442e3e30b6" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.text_prompt(text='cap')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KYW79zD0OKsv" + }, + "source": [ + "**NOTE:** `prompt_process.text_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "zODY5xU1LMCb", + "outputId": "a01540a7-deb3-4555-916c-2ac59928e0f3" + }, + "source": [ + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XFOUWUMZEk7m" + }, + "source": [ + "## SAM vs FastSAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "7PKBZ7Hhahhw" + }, + "source": [ + "IMAGE_PATH = f\"{HOME}/data/robot.jpeg\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c48hRY7dVJeL" + }, + "source": [ + "### Load SAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "PdKpFCCmaETG" + }, + "source": [ + "MODEL_TYPE = \"vit_h\"\n", + "\n", + "sam = sam_model_registry[MODEL_TYPE](checkpoint=SAM_SAM_CHECKPOINT_PATH).to(device=DEVICE)\n", + "mask_generator = SamAutomaticMaskGenerator(sam)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V5bPGvtBaewO" + }, + "source": [ + "### SAM inference" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "0HgWsk1yalow" + }, + "source": [ + "image_bgr = cv2.imread(IMAGE_PATH)\n", + "image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)\n", + "\n", + "sam_result = mask_generator.generate(image_rgb)\n", + "sam_detections = sv.Detections.from_sam(sam_result=sam_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "t03RrSzMasN6" + }, + "source": [ + "### FastSAM inference" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "xuFGLw8MavQI", + "outputId": "b0118973-1e29-419d-b7c0-2933b97d7ce5" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()\n", + "masks = masks.cpu().numpy().astype(bool)\n", + "xyxy = sv.mask_to_xyxy(masks=masks)\n", + "fast_sam_detections = sv.Detections(xyxy=xyxy, mask=masks)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IYn8xKySbBnc" + }, + "source": [ + "### FastSAM vs. SAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 321 + }, + "id": "c2Qj7sLjbDvo", + "outputId": "e85e3ed3-8b90-409d-cf95-877a6a9b2034" + }, + "source": [ + "mask_annotator = sv.MaskAnnotator()\n", + "\n", + "sam_result = mask_annotator.annotate(scene=image_bgr.copy(), detections=sam_detections)\n", + "fast_sam_result = mask_annotator.annotate(scene=image_bgr.copy(), detections=fast_sam_detections)\n", + "\n", + "sv.plot_images_grid(\n", + " images=[sam_result, fast_sam_result],\n", + " grid_size=(1, 2),\n", + " titles=['SAM', 'FastSAM']\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MfU4NQiGWQMC" + }, + "source": [ + "**NOTE:** There is a lot going on in our test image. Let's plot our masks against a blank background." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 321 + }, + "id": "PIrywlF5QRPz", + "outputId": "8e7c84ae-7869-4523-847a-0678fd1aea53" + }, + "source": [ + "mask_annotator = sv.MaskAnnotator()\n", + "\n", + "sam_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=sam_detections)\n", + "fast_sam_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=fast_sam_detections)\n", + "\n", + "sv.plot_images_grid(\n", + " images=[sam_result, fast_sam_result],\n", + " grid_size=(1, 2),\n", + " titles=['SAM', 'FastSAM']\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "51Fs3sDGQmIt" + }, + "source": [ + "### Mask diff\n", + "\n", + "**NOTE:** Let's try to understand which masks generated by SAM were not generated by FastSAM." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "O5bIUAhYQrqZ" + }, + "source": [ + "def compute_iou(mask1, mask2):\n", + " intersection = np.logical_and(mask1, mask2)\n", + " union = np.logical_or(mask1, mask2)\n", + " return np.sum(intersection) / np.sum(union)\n", + "\n", + "def filter_masks(mask_array1, mask_array2, threshold):\n", + " return np.array([mask1 for mask1 in mask_array1 if max(compute_iou(mask1, mask2) for mask2 in mask_array2) < threshold])" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 420 + }, + "id": "1GM4bL9sRbOm", + "outputId": "421fb4f5-7a42-461b-ba1e-7cd46d2d7bd7" + }, + "source": [ + "diff_masks = filter_masks(sam_detections.mask, fast_sam_detections.mask, 0.5)\n", + "diff_xyxy = sv.mask_to_xyxy(masks=diff_masks)\n", + "diff_detections = sv.Detections(xyxy=diff_xyxy, mask=diff_masks)\n", + "\n", + "mask_annotator = sv.MaskAnnotator()\n", + "diff_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=diff_detections)\n", + "sv.plot_image(image=diff_result, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0lPZAZZulhF8" + }, + "source": [ + "## Roboflow Benchmark Dataset" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LWN6XwPuSZBY", + "outputId": "86b18c0e-f6d6-486b-eee6-d004f13f531e" + }, + "source": [ + "%cd {HOME}\n", + "\n", + "roboflow.login()\n", + "rf = Roboflow()\n", + "\n", + "project = rf.workspace(\"roboticfish\").project(\"underwater_object_detection\")\n", + "dataset = project.version(10).download(\"yolov8\")" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "T6-PaxWXTZ69" + }, + "source": [ + "IMAGE_PATH = os.path.join(dataset.location, 'train', 'images', 'IMG_2311_jpeg_jpg.rf.09ae6820eaff21dc838b1f9b6b20342b.jpg')" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "p-gY5jf1Tife", + "outputId": "630d35d8-26f4-4342-8d82-c7b61be26fe8" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.text_prompt(text='Penguin')" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "3tBIH0yubHsW", + "outputId": "fc4d0a6a-53c1-4a2b-91f0-75c8f78e10fd" + }, + "source": [ + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gP9gEw9Tp8GJ" + }, + "source": [ + "## 🏆 Congratulations\n", + "\n", + "### Learning Resources\n", + "\n", + "Roboflow has produced many resources that you may find interesting as you advance your knowledge of computer vision:\n", + "\n", + "- [Roboflow Notebooks](https://github.com/roboflow/notebooks): A repository of over 20 notebooks that walk through how to train custom models with a range of model types, from YOLOv7 to SegFormer.\n", + "- [Roboflow YouTube](https://www.youtube.com/c/Roboflow): Our library of videos featuring deep dives into the latest in computer vision, detailed tutorials that accompany our notebooks, and more.\n", + "- [Roboflow Discuss](https://discuss.roboflow.com/): Have a question about how to do something on Roboflow? Ask your question on our discussion forum.\n", + "- [Roboflow Models](https://roboflow.com): Learn about state-of-the-art models and their performance. Find links and tutorials to guide your learning.\n", + "\n", + "### Convert data formats\n", + "\n", + "Roboflow provides free utilities to convert data between dozens of popular computer vision formats. Check out [Roboflow Formats](https://roboflow.com/formats) to find tutorials on how to convert data between formats in a few clicks.\n", + "\n", + "### Connect computer vision to your project logic\n", + "\n", + "[Roboflow Templates](https://roboflow.com/templates) is a public gallery of code snippets that you can use to connect computer vision to your project logic. Code snippets range from sending emails after inference to measuring object distance between detections." + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 19 }, + { modified: 19, original: 20 }, + { modified: 20, original: 22 }, + { modified: 21, original: 23 }, + { modified: 22, original: 24 }, + { modified: 23, original: 25 }, + { modified: 24, original: 26 }, + { modified: 25, original: 27 }, + { modified: 26, original: 28 }, + { modified: 27, original: 29 }, + { modified: 28, original: 30 }, + { modified: 29, original: 31 }, + { modified: 30, original: 32 }, + { modified: 31, original: 33 }, + { modified: 32, original: 34 }, + { modified: 33, original: 35 }, + { modified: 34, original: 36 }, + { modified: 35, original: 37 }, + { modified: 36, original: 38 }, + { modified: 37, original: 39 }, + { modified: 38, original: 40 }, + { modified: 39, original: 41 }, + { modified: 40, original: 42 }, + { modified: 41, original: 43 }, + { modified: 42, original: 44 }, + { modified: 43, original: 45 }, + { modified: 44, original: 46 }, + { modified: 45, original: 47 }, + { modified: 46, original: 48 }, + { modified: 47, original: 49 }, + { modified: 48, original: 50 }, + { modified: 49, original: 51 }, + { modified: 50, original: 52 }, + { modified: 51, original: 53 }, + { modified: 52, original: 54 }, + { modified: 53, original: 55 }, + { modified: 54, original: 56 }, + { modified: 55, original: 57 }, + { modified: 56, original: 58 }, + { modified: 57, original: 59 }, + { modified: 58, original: 60 }, + { modified: 59, original: 61 }, + { modified: 60, original: 62 }, + { modified: 61, original: 63 }, + { modified: 62, original: 64 }, + { modified: 63, original: 65 }, + { modified: 64, original: 66 }, + { modified: 65, original: 67 }, + { modified: 66, original: 68 }, + { modified: 67, original: 69 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 12, originalLength: 1, modifiedStart: 12, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 15, originalLength: 1, modifiedStart: 15, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 18, originalLength: 1, modifiedStart: 18, modifiedLength: 0 } satisfies IDiffChange, + { originalStart: 20, originalLength: 1, modifiedStart: 19, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 21, originalLength: 1, modifiedStart: 20, modifiedLength: 0 } satisfies IDiffChange, + { originalStart: 22, originalLength: 1, modifiedStart: 20, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 23, originalLength: 1, modifiedStart: 21, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 26, originalLength: 1, modifiedStart: 24, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 37, originalLength: 1, modifiedStart: 35, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 44, originalLength: 1, modifiedStart: 42, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 48, originalLength: 1, modifiedStart: 46, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 56, originalLength: 1, modifiedStart: 54, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 58, originalLength: 1, modifiedStart: 56, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 60, originalLength: 1, modifiedStart: 58, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 63, originalLength: 1, modifiedStart: 61, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 68, originalLength: 1, modifiedStart: 66, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 53, originalLength: 0, modifiedStart: 53, modifiedLength: 2 } satisfies IDiffChange, + ]); + + }); + test('Insert a few cells (one on top), modify a few and completely misaligned', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "from cachetools import cached, TTLCache\n", + "from dotenv import load_dotenv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "GITHUB_API_URL = \"https://api.github.com\"\n", + "REPO_OWNER = \"microsoft\"\n", + "ISSUE = 123 # Replace with your milestone number\n", + "load_dotenv()\n", + "TOKEN = os.getenv(\"GH\")\n", + "\n", + "headers = {\n", + " \"Authorization\": f\"Bearer {TOKEN}\",\n", + " \"Accept\": \"application/vnd.github+json\"\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import csv\n", + "from datetime import datetime\n", + "import os\n", + "\n", + "def append_information(repo, open_issues_count, ):\n", + " file_exists = os.path.isfile(filename)\n", + " timestamp = datetime.utcnow().isoformat()\n", + "\n", + " # Open the file in append mode\n", + " with open(filename, 'a', newline='', encoding='utf-8') as csvfile:\n", + " writer = csv.writer(csvfile)\n", + "\n", + " # If the file didn't exist before, write the header row\n", + " if not file_exists:\n", + " writer.writerow([\n", + " \"timestamp\",\n", + " \"repo\",\n", + " \"open_issues_count\",\n", + " \"open_prs_count\",\n", + " \"bug_issues_count\",\n", + " \"feature_request_issues_count\",\n", + " \"invalid_issues_count\"\n", + " ])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_repo_issue_stats(owner, repo, token):\n", + " # Common GitHub GraphQL endpoint\n", + " url = \"https://api.github.com/graphql\"\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {token}\",\n", + " \"Accept\": \"application/vnd.github.v4+json\"\n", + " }\n", + "\n", + " # First query: Get various issue/PR counts from the repository object\n", + " query_repo = \"\"\"\n", + " query ($owner: String!, $name: String!) {\n", + " repository(owner: $owner, name: $name) {\n", + " issues(states: OPEN) {\n", + " totalCount\n", + " }\n", + " pullRequests(states: OPEN) {\n", + " totalCount\n", + " }\n", + " bugIssues: issues(states: OPEN, labels: [\"bug\"]) {\n", + " totalCount\n", + " }\n", + " featureRequestIssues: issues(states: OPEN, labels: [\"feature-request\"]) {\n", + " totalCount\n", + " }\n", + " }\n", + " }\n", + " \"\"\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for REPO in REPOS:\n", + " stats = get_repo_issue_stats(OWNER, REPO, TOKEN)\n", + " print(stats)\n", + "\n", + " append_metrics_to_csv(\n", + " filename=\"repo_metrics.csv\",\n", + " repo=f\"{OWNER}/{REPO}\",\n", + " open_issues_count=stats['open_issues_count'],\n", + " open_prs_count=stats['open_prs_count'],\n", + " bug_issues_count=stats['bug_issues_count'],\n", + " feature_request_issues_count=stats['feature_request_issues_count'],\n", + " invalid_issues_count=stats['invalid_issues_count']\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import plotly.graph_objs as go\n", + "from plotly.subplots import make_subplots\n", + "\n", + "def plot_repo_metrics(repo, csv_filename=\"repo_metrics.csv\"):\n", + " # Load the CSV data\n", + " df = pd.read_csv(csv_filename)\n", + "\n", + " fig.add_trace(go.Scatter(\n", + " x=df_repo['timestamp'],\n", + " y=df_repo['open_issues_count'],\n", + " mode='lines+markers',\n", + " name='Open Issues',\n", + " hovertemplate='Open Issues: %{y}
Time: %{x}'\n", + " ))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for REPO in REPOS:\n", + " plot_repo_metrics(f\"{OWNER}/{REPO}\")" + ] + } + ] + .map(fromJupyterCell) + , + [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Fetch Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import sys\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "from cachetools import cached, TTLCache\n", + "from dotenv import load_dotenv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "GITHUB_API_URL = \"https://api.github.com\"\n", + "REPO_OWNER = \"microsoft\"\n", + "ISSUE = 123\n", + "load_dotenv()\n", + "TOKEN = os.getenv(\"GH\")\n", + "\n", + "headers = {\n", + " \"Authorization\": f\"Bearer {TOKEN}\",\n", + " \"Accept\": \"application/vnd.github+json\"\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import csv\n", + "from datetime import datetime\n", + "import os\n", + "\n", + "def append_information(repo, open_issues_count, ):\n", + " file_exists = os.path.isfile(filename)\n", + " timestamp = datetime.utcnow().isoformat()\n", + "\n", + " # Open the file in append mode\n", + " with open(filename, 'a', newline='', encoding='utf-8') as csvfile:\n", + " writer = csv.writer(csvfile)\n", + "\n", + " # If the file didn't exist before, write the header row\n", + " if not file_exists:\n", + " writer.writerow([\n", + " \"timestamp\",\n", + " \"repo\",\n", + " \"open_issues_count\",\n", + " \"open_prs_count\",\n", + " \"bug_issues_count\",\n", + " \"feature_request_issues_count\",\n", + " \"invalid_issues_count\"\n", + " ])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_repo_issue_stats(owner, repo, token):\n", + " # Common GitHub GraphQL endpoint\n", + " url = \"https://api.github.com/graphql\"\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {token}\",\n", + " \"Accept\": \"application/vnd.github.v4+json\"\n", + " }\n", + "\n", + " # First query: Get various issue/PR counts from the repository object\n", + " query_repo = \"\"\"\n", + " query ($owner: String!, $name: String!) {\n", + " repository(owner: $owner, name: $name) {\n", + " issues(states: OPEN) {\n", + " totalCount\n", + " }\n", + " pullRequests(states: OPEN) {\n", + " totalCount\n", + " }\n", + " bugIssues: issues(states: OPEN, labels: [\"bug\"]) {\n", + " totalCount\n", + " }\n", + " featureRequestIssues: issues(states: OPEN, labels: [\"feature-request\"]) {\n", + " totalCount\n", + " }\n", + " }\n", + " }\n", + " \"\"\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for REPO in REPOS:\n", + " stats = get_repo_issue_stats(OWNER, REPO, TOKEN)\n", + " print(stats)\n", + "\n", + " append_metrics_to_csv(\n", + " filename=\"repo_metrics.csv\",\n", + " repo=f\"{OWNER}/{REPO}\",\n", + " open_issues_count=stats['open_issues_count'],\n", + " open_prs_count=stats['open_prs_count'],\n", + " bug_issues_count=stats['bug_issues_count'],\n", + " feature_request_issues_count=stats['feature_request_issues_count'],\n", + " invalid_issues_count=stats['invalid_issues_count']\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import plotly.graph_objs as go\n", + "from plotly.subplots import make_subplots\n", + "\n", + "def plot_repo_metrics(repo, csv_filename=\"repo_metrics.csv\"):\n", + " # Load the CSV data\n", + " df = pd.read_csv(csv_filename)\n", + "\n", + " fig.add_trace(go.Scatter(\n", + " x=df_repo['timestamp'],\n", + " y=df_repo['open_issues_count'],\n", + " mode='lines+markers',\n", + " name='Open Issues',\n", + " hovertemplate='Open Issues: %{y}
Time: %{x}'\n", + " ))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Header" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for REPO in REPOS:\n", + " plot_repo_metrics(f\"{OWNER}/{REPO}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_repo_metrics(f\"{OWNER}/vscode\")" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 1 }, + { modified: 3, original: 2 }, + { modified: 4, original: 3 }, + { modified: 5, original: 4 }, + { modified: 6, original: 5 }, + { modified: 7, original: 6 }, + { modified: 8, original: -1 }, + { modified: 9, original: 7 }, + { modified: 10, original: -1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 0, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 1, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 7, originalLength: 0, modifiedStart: 8, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 8, originalLength: 0, modifiedStart: 10, modifiedLength: 1 } satisfies IDiffChange, + ]); + + + }); + + let uriCounter = 0; + async function mapCells(original: MockNotebookCell[], modified: MockNotebookCell[]) { + const mapping = matchCellBasedOnSimilarties(modified.map(cellToDto), original.map(cellToDto)).map(mapping => ({ modified: mapping.modified, original: mapping.original })); + const originalUri = URI.parse(`file://original${uriCounter++}.ipynb`); + const modifiedUri = URI.parse(`file://modified${uriCounter++}.ipynb`); + worker.$acceptNewModel(originalUri.toString(), {}, {}, original.map((cell, index) => cellToMainCellTdo(originalUri, cell, index))); + worker.$acceptNewModel(modifiedUri.toString(), {}, {}, modified.map((cell, index) => cellToMainCellTdo(modifiedUri, cell, index))); + const diff = await worker.$computeDiff(originalUri.toString(), modifiedUri.toString()); + diff.cellsDiff.changes = diff.cellsDiff.changes.map(change => { + return { + originalStart: change.originalStart, + originalLength: change.originalLength, + modifiedStart: change.modifiedStart, + modifiedLength: change.modifiedLength, + }; + }); + return { mapping, diff }; + } + + + function cellToDto(cell: MockNotebookCell): { getValue(): string; getLinesContent(): string[]; cellKind: CellKind } { + return { + cellKind: cell[2], + getValue: () => cell[0].join('\n'), + getLinesContent: () => cell[0] + }; + } + + function cellToMainCellTdo(notebookUri: URI, cell: MockNotebookCell, index: number): IMainCellDto { + return { + source: cell[0], + language: cell[1], + cellKind: cell[2], + outputs: cell[3] || [], + metadata: cell[4], + eol: '\n', + handle: index, + url: URI.from({ scheme: 'file', path: notebookUri.path, fragment: `cell/${index}` }).toString(), + versionId: 0, + internalMetadata: undefined + } satisfies IMainCellDto; + + } + type MockNotebookCell = [ + lines: string[], + lang: string, + kind: CellKind, + output?: IOutputDto[], + metadata?: NotebookCellMetadata, + ]; + + function fromJupyterCell(cell: { cell_type: string; source: string[] }): MockNotebookCell { + return [ + cell.source, + cell.cell_type === 'code' ? 'python' : 'markdown', + cell.cell_type === 'code' ? CellKind.Code : CellKind.Markup, + ]; + } +}); + + diff --git a/src/vs/workbench/contrib/performance/electron-sandbox/rendererAutoProfiler.ts b/src/vs/workbench/contrib/performance/electron-sandbox/rendererAutoProfiler.ts index 274d3340b87..5286ecc047b 100644 --- a/src/vs/workbench/contrib/performance/electron-sandbox/rendererAutoProfiler.ts +++ b/src/vs/workbench/contrib/performance/electron-sandbox/rendererAutoProfiler.ts @@ -38,7 +38,7 @@ export class RendererProfiling { } timerService.perfBaseline.then(perfBaseline => { - _logService.info(`[perf] Render performance baseline is ${perfBaseline}ms`); + (_environmentService.isBuilt ? _logService.info : _logService.trace).apply(_logService, [`[perf] Render performance baseline is ${perfBaseline}ms`]); if (perfBaseline < 0) { // too slow diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts index d896d8650da..af01934653d 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts @@ -26,6 +26,7 @@ import { assertIsDefined } from '../../../../base/common/types.js'; import { isEqual } from '../../../../base/common/resources.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; import { DEFINE_KEYBINDING_EDITOR_CONTRIB_ID, IDefineKeybindingEditorContribution } from '../../../services/preferences/common/preferences.js'; +import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; const NLS_KB_LAYOUT_ERROR_MESSAGE = nls.localize('defineKeybinding.kbLayoutErrorMessage', "You won't be able to produce this key combination under your current keyboard layout."); @@ -88,13 +89,14 @@ class DefineKeybindingEditorContribution extends Disposable implements IDefineKe export class KeybindingEditorDecorationsRenderer extends Disposable { private _updateDecorations: RunOnceScheduler; - private readonly _dec = this._editor.createDecorationsCollection(); + private readonly _dec: IEditorDecorationsCollection; constructor( private _editor: ICodeEditor, @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); + this._dec = this._editor.createDecorationsCollection(); this._updateDecorations = this._register(new RunOnceScheduler(() => this._updateDecorationsNow(), 500)); diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 9bdbebe0a01..d69acc0a677 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -24,7 +24,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from '../../../browser/actions/workspaceCommands.js'; import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; -import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { ResourceContextKey, RemoteNameContext, WorkbenchStateContext } from '../../../common/contextkeys.js'; @@ -39,7 +39,6 @@ import { PreferencesContribution } from '../common/preferencesContribution.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { KeybindingsEditorInput } from '../../../services/preferences/browser/keybindingsEditorInput.js'; import { DEFINE_KEYBINDING_EDITOR_CONTRIB_ID, IDefineKeybindingEditorContribution, IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { SettingsEditor2Input } from '../../../services/preferences/common/preferencesEditorInput.js'; @@ -1239,6 +1238,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } class SettingsEditorTitleContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.settingsEditorTitleBarActions'; + constructor( @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @@ -1249,13 +1251,13 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo private registerSettingsEditorTitleActions() { const registerOpenUserSettingsEditorFromJsonActionDisposables = this._register(new MutableDisposable()); - const openUserSettingsEditorWhen = ContextKeyExpr.and( - ContextKeyExpr.or( - ResourceContextKey.Resource.isEqualTo(this.userDataProfileService.currentProfile.settingsResource.toString()), - ResourceContextKey.Resource.isEqualTo(this.userDataProfilesService.defaultProfile.settingsResource.toString())), - ContextKeyExpr.not('isInDiffEditor')); const registerOpenUserSettingsEditorFromJsonAction = () => { - registerOpenUserSettingsEditorFromJsonActionDisposables.value = undefined; + const openUserSettingsEditorWhen = ContextKeyExpr.and( + CONTEXT_SETTINGS_EDITOR.toNegated(), + ContextKeyExpr.or( + ResourceContextKey.Resource.isEqualTo(this.userDataProfileService.currentProfile.settingsResource.toString()), + ResourceContextKey.Resource.isEqualTo(this.userDataProfilesService.defaultProfile.settingsResource.toString())), + ContextKeyExpr.not('isInDiffEditor')); registerOpenUserSettingsEditorFromJsonActionDisposables.value = registerAction2(class extends Action2 { constructor() { super({ @@ -1271,9 +1273,9 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo }); } run(accessor: ServicesAccessor, ...args: unknown[]) { - const sanatizedArgs = sanitizeOpenSettingsArgs(args[0]); + const sanitizedArgs = sanitizeOpenSettingsArgs(args[0]); const groupId = getEditorGroupFromArguments(accessor, args)?.id; - return accessor.get(IPreferencesService).openUserSettings({ jsonEditor: false, ...sanatizedArgs, groupId }); + return accessor.get(IPreferencesService).openUserSettings({ jsonEditor: false, ...sanitizedArgs, groupId }); } }); }; @@ -1284,7 +1286,7 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo registerOpenUserSettingsEditorFromJsonAction(); })); - const openSettingsJsonWhen = ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()); + const openSettingsJsonWhen = ContextKeyExpr.and(CONTEXT_SETTINGS_JSON_EDITOR.toNegated(), CONTEXT_SETTINGS_EDITOR); this._register(registerAction2(class extends Action2 { constructor() { super({ @@ -1316,10 +1318,9 @@ function getEditorGroupFromArguments(accessor: ServicesAccessor, args: unknown[] return context.groupedEditors[0]?.group; } -const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); registerWorkbenchContribution2(PreferencesActionsContribution.ID, PreferencesActionsContribution, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(PreferencesContribution.ID, PreferencesContribution, WorkbenchPhase.BlockStartup); -workbenchContributionsRegistry.registerWorkbenchContribution(SettingsEditorTitleContribution, LifecyclePhase.Restored); +registerWorkbenchContribution2(SettingsEditorTitleContribution.ID, SettingsEditorTitleContribution, WorkbenchPhase.AfterRestored); registerEditorContribution(SettingsEditorContribution.ID, SettingsEditorContribution, EditorContributionInstantiation.AfterFirstRender); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index c8f0b595562..1d6c0b83da2 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -787,7 +787,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc class WorkspaceConfigurationRenderer extends Disposable { private static readonly supportedKeys = ['folders', 'tasks', 'launch', 'extensions', 'settings', 'remoteAuthority', 'transient']; - private readonly decorations = this.editor.createDecorationsCollection(); + private readonly decorations: editorCommon.IEditorDecorationsCollection; private renderingDelayer: Delayer = new Delayer(200); constructor(private editor: ICodeEditor, private workspaceSettingsEditorModel: SettingsEditorModel, @@ -795,6 +795,7 @@ class WorkspaceConfigurationRenderer extends Disposable { @IMarkerService private readonly markerService: IMarkerService ) { super(); + this.decorations = this.editor.createDecorationsCollection(); this._register(this.editor.getModel()!.onDidChangeContent(() => this.renderingDelayer.trigger(() => this.render()))); } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 57f59b72d5d..52ef9dbd446 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISettingsEditorModel, ISetting, ISettingsGroup, ISearchResult, IGroupFilter, SettingMatchType, ISettingMatch, SettingKeyMatchTypes } from '../../../services/preferences/common/preferences.js'; +import { ISettingsEditorModel, ISetting, ISettingsGroup, ISearchResult, IGroupFilter, SettingMatchType, ISettingMatch, SettingKeyMatchTypes, ISettingMatcher } from '../../../services/preferences/common/preferences.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { distinct } from '../../../../base/common/arrays.js'; import * as strings from '../../../../base/common/strings.js'; @@ -82,9 +82,6 @@ function cleanFilter(filter: string): string { } export class LocalSearchProvider implements ISearchProvider { - static readonly EXACT_MATCH_SCORE = 10000; - static readonly START_SCORE = 1000; - constructor( private _filter: string, @IConfigurationService private readonly configurationService: IConfigurationService @@ -97,55 +94,46 @@ export class LocalSearchProvider implements ISearchProvider { return Promise.resolve(null); } - let orderedScore = LocalSearchProvider.START_SCORE; // Sort is not stable - const useNewKeyMatchAlgorithm = this.configurationService.getValue('workbench.settings.useWeightedKeySearch') === true; - const settingMatcher = (setting: ISetting) => { - const { matches, matchType, keyMatchScore } = new SettingMatches( + const settingMatcher: ISettingMatcher = (setting: ISetting) => { + let { matches, matchType, keyMatchScore } = new SettingMatches( this._filter, setting, true, (filter, setting) => preferencesModel.findValueMatches(filter, setting), - useNewKeyMatchAlgorithm, this.configurationService ); if (matchType === SettingMatchType.None || matches.length === 0) { return null; } - - const score = strings.equalsIgnoreCase(this._filter, setting.key) ? - LocalSearchProvider.EXACT_MATCH_SCORE : - orderedScore--; + if (strings.equalsIgnoreCase(this._filter, setting.key)) { + matchType = SettingMatchType.ExactMatch; + } return { matches, matchType, keyMatchScore, - score + score: 0 // only used for RemoteSearchProvider matches. }; }; const filterMatches = preferencesModel.filterSettings(this._filter, this.getGroupFilter(this._filter), settingMatcher); - const exactMatch = filterMatches.find(m => m.score === LocalSearchProvider.EXACT_MATCH_SCORE); + const exactMatch = filterMatches.find(m => m.matchType === SettingMatchType.ExactMatch); if (exactMatch) { return Promise.resolve({ filterMatches: [exactMatch], exactMatch: true }); - } else if (useNewKeyMatchAlgorithm) { - // Check the top key match type. - const topKeyMatchType = Math.max(...filterMatches.map(m => (m.matchType & SettingKeyMatchTypes))); - // Always allow description matches as part of https://github.com/microsoft/vscode/issues/239936. - const alwaysAllowedMatchTypes = SettingMatchType.DescriptionOrValueMatch | SettingMatchType.LanguageTagSettingMatch; - const filteredMatches = filterMatches.filter(m => (m.matchType & topKeyMatchType) || (m.matchType & alwaysAllowedMatchTypes)); - return Promise.resolve({ - filterMatches: filteredMatches, - exactMatch: false - }); - } else { - return Promise.resolve({ - filterMatches: filterMatches, - exactMatch: false - }); } + + // Check the top key match type. + const topKeyMatchType = Math.max(...filterMatches.map(m => (m.matchType & SettingKeyMatchTypes))); + // Always allow description matches as part of https://github.com/microsoft/vscode/issues/239936. + const alwaysAllowedMatchTypes = SettingMatchType.DescriptionOrValueMatch | SettingMatchType.LanguageTagSettingMatch; + const filteredMatches = filterMatches.filter(m => (m.matchType & topKeyMatchType) || (m.matchType & alwaysAllowedMatchTypes)); + return Promise.resolve({ + filterMatches: filteredMatches, + exactMatch: false + }); } private getGroupFilter(filter: string): IGroupFilter { @@ -170,7 +158,6 @@ export class SettingMatches { setting: ISetting, private searchDescription: boolean, valuesMatcher: (filter: string, setting: ISetting) => IRange[], - private useNewKeyMatchAlgorithm: boolean, private readonly configurationService: IConfigurationService ) { this.matches = distinct(this._findMatchesInSetting(searchString, setting), (match) => `${match.startLineNumber}_${match.startColumn}_${match.endLineNumber}_${match.endColumn}_`); @@ -205,68 +192,52 @@ export class SettingMatches { const settingKeyAsWords: string = this._keyToLabel(setting.key); const queryWords = new Set(searchString.split(' ')); for (const word of queryWords) { - // Check if the key contains the word. - // Force contiguous matching iff we're using the new algorithm. - const keyMatches = matchesWords(word, settingKeyAsWords, this.useNewKeyMatchAlgorithm); + // Check if the key contains the word. Use contiguous search. + const keyMatches = matchesWords(word, settingKeyAsWords, true); if (keyMatches?.length) { keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); } } - if (this.useNewKeyMatchAlgorithm) { - // New key match algorithm - if (keyMatchingWords.size === queryWords.size) { - // All words in the query matched with something in the setting key. - // Matches "edit format on paste" to "editor.formatOnPaste". - this.matchType |= SettingMatchType.AllWordsInSettingsLabel; - } else if (keyMatchingWords.size >= 2) { - // Matches "edit paste" to "editor.formatOnPaste". - // The if statement reduces noise by preventing "editor formatonpast" from matching all editor settings. - this.matchType |= SettingMatchType.ContiguousWordsInSettingsLabel; - this.keyMatchScore = keyMatchingWords.size; - } - const searchStringAlphaNumeric = this._toAlphaNumeric(searchString); - const keyAlphaNumeric = this._toAlphaNumeric(setting.key); - const keyIdMatches = matchesContiguousSubString(searchStringAlphaNumeric, keyAlphaNumeric); - if (keyIdMatches?.length) { - // Matches "editorformatonp" to "editor.formatonpaste". - keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); - this.matchType |= SettingMatchType.ContiguousQueryInSettingId; - } + if (keyMatchingWords.size === queryWords.size) { + // All words in the query matched with something in the setting key. + // Matches "edit format on paste" to "editor.formatOnPaste". + this.matchType |= SettingMatchType.AllWordsInSettingsLabel; + } else if (keyMatchingWords.size >= 2) { + // Matches "edit paste" to "editor.formatOnPaste". + // The if statement reduces noise by preventing "editor formatonpast" from matching all editor settings. + this.matchType |= SettingMatchType.ContiguousWordsInSettingsLabel; + this.keyMatchScore = keyMatchingWords.size; + } + const searchStringAlphaNumeric = this._toAlphaNumeric(searchString); + const keyAlphaNumeric = this._toAlphaNumeric(setting.key); + const keyIdMatches = matchesContiguousSubString(searchStringAlphaNumeric, keyAlphaNumeric); + if (keyIdMatches?.length) { + // Matches "editorformatonp" to "editor.formatonpaste". + keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); + this.matchType |= SettingMatchType.ContiguousQueryInSettingId; + } - // Fall back to non-contiguous searches if nothing matched yet. - if (this.matchType === SettingMatchType.None) { - keyMatchingWords.clear(); - for (const word of queryWords) { - const keyMatches = matchesWords(word, settingKeyAsWords, false); - if (keyMatches?.length) { - keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); - } - } - if (keyMatchingWords.size >= 2 || (keyMatchingWords.size === 1 && queryWords.size === 1)) { - // Matches "edforonpas" to "editor.formatOnPaste". - // The if statement reduces noise by preventing "editor fomonpast" from matching all editor settings. - this.matchType |= SettingMatchType.NonContiguousWordsInSettingsLabel; - this.keyMatchScore = keyMatchingWords.size; - } else { - const keyIdMatches = matchesSubString(searchStringAlphaNumeric, keyAlphaNumeric); - if (keyIdMatches?.length) { - // Matches "edfmonpas" to "editor.formatOnPaste". - keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); - this.matchType |= SettingMatchType.NonContiguousQueryInSettingId; - } + // Fall back to non-contiguous key (ID) searches if nothing matched yet. + if (this.matchType === SettingMatchType.None) { + keyMatchingWords.clear(); + for (const word of queryWords) { + const keyMatches = matchesWords(word, settingKeyAsWords, false); + if (keyMatches?.length) { + keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); } } - } else { - // Old key match algorithm - if (keyMatchingWords.size) { + if (keyMatchingWords.size >= 2 || (keyMatchingWords.size === 1 && queryWords.size === 1)) { + // Matches "edforonpas" to "editor.formatOnPaste". + // The if statement reduces noise by preventing "editor fomonpast" from matching all editor settings. this.matchType |= SettingMatchType.NonContiguousWordsInSettingsLabel; this.keyMatchScore = keyMatchingWords.size; - } - const keyIdMatches = matchesContiguousSubString(searchString, setting.key); - if (keyIdMatches?.length) { - // Handles cases such as "editor.formatonpaste" where the user tries searching for the ID. - keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); - this.matchType |= SettingMatchType.ContiguousQueryInSettingId; + } else { + const keyIdMatches = matchesSubString(searchStringAlphaNumeric, keyAlphaNumeric); + if (keyIdMatches?.length) { + // Matches "edfmonpas" to "editor.formatOnPaste". + keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); + this.matchType |= SettingMatchType.NonContiguousQueryInSettingId; + } } } @@ -280,11 +251,9 @@ export class SettingMatches { } // Description search - // Old algorithm: search the description if we haven't matched anything yet. - // New algorithm: search the description if we found non-contiguous key matches at best. + // Search the description if we found non-contiguous key matches at best. const hasContiguousKeyMatchTypes = this.matchType >= SettingMatchType.ContiguousWordsInSettingsLabel; - const checkDescription = (!this.useNewKeyMatchAlgorithm && this.matchType === SettingMatchType.None) || (this.useNewKeyMatchAlgorithm && !hasContiguousKeyMatchTypes); - if (this.searchDescription && checkDescription) { + if (this.searchDescription && !hasContiguousKeyMatchTypes) { for (const word of queryWords) { // Search the description lines. for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { @@ -304,10 +273,8 @@ export class SettingMatches { // Value search // Check if the value contains all the words. - // Old algorithm: always search the values. - // New algorithm: search the values if we found non-contiguous key matches at best. - const checkValue = !this.useNewKeyMatchAlgorithm || !hasContiguousKeyMatchTypes; - if (checkValue) { + // Search the values if we found non-contiguous key matches at best. + if (!hasContiguousKeyMatchTypes) { if (setting.enum?.length) { // Search all string values of enums. for (const option of setting.enum) { diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 84321a355d9..9bc4c943238 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -37,6 +37,7 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; export class FolderSettingsActionViewItem extends BaseActionViewItem { @@ -500,13 +501,14 @@ export class EditPreferenceWidget extends Disposable { private _line: number = -1; private _preferences: T[] = []; - private readonly _editPreferenceDecoration = this.editor.createDecorationsCollection(); + private readonly _editPreferenceDecoration: IEditorDecorationsCollection; private readonly _onClick = this._register(new Emitter()); readonly onClick: Event = this._onClick.event; constructor(private editor: ICodeEditor) { super(); + this._editPreferenceDecoration = this.editor.createDecorationsCollection(); this._register(this.editor.onMouseDown((e: IEditorMouseEvent) => { if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN || e.target.detail.isAfterLines || !this.isVisible()) { return; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 777931961e1..5b1143c5dbf 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -413,6 +413,7 @@ export class SettingsEditor2 extends EditorPane { // Don't block setInput on render (which can trigger an async search) this.onConfigUpdate(undefined, true).then(() => { + // This event runs when the editor closes. this.inputChangeListener.value = input.onWillDispose(() => { this.searchWidget.setValue(''); }); @@ -1160,11 +1161,6 @@ export class SettingsEditor2 extends EditorPane { this.renderTree(key, isManualReset); this.pendingSettingUpdate = null; - // Only log 1% of modification events to reduce the volume of data - if (Math.random() >= 0.01) { - return; - } - const reportModifiedProps = { key, query, @@ -1545,7 +1541,9 @@ export class SettingsEditor2 extends EditorPane { private refreshSingleElement(element: SettingsTreeSettingElement): void { if (this.isVisible()) { - this.settingsTree.rerender(element); + if (!element.setting.deprecationMessage || element.isConfigured) { + this.settingsTree.rerender(element); + } } } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index 678042ec78b..c669643a71a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -145,7 +145,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const content = localize('trustLabel', "The setting value can only be applied in a trusted workspace."); const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, content, target: workspaceTrustElement, @@ -186,7 +186,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const syncIgnoredHoverContent = localize('syncIgnoredTitle', "This setting is ignored during sync"); const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, content: syncIgnoredHoverContent, target: syncIgnoredElement @@ -329,7 +329,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const content = isPreviewSetting ? PREVIEW_INDICATOR_DESCRIPTION : EXPERIMENTAL_INDICATOR_DESCRIPTION; const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, content, target: this.previewIndicator.element @@ -374,7 +374,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { this.scopeOverridesIndicator.label.text = '$(briefcase) ' + localize('policyLabelText', "Managed by organization"); const content = localize('policyDescription', "This setting is managed by your organization and its actual value cannot be changed."); const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, content, actions: [{ @@ -396,7 +396,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const content = localize('applicationSettingDescription', "The setting is not specific to the current profile, and will retain its value when switching profiles."); const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, content, target: this.scopeOverridesIndicator.element @@ -512,7 +512,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { } const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ content: new MarkdownString().appendMarkdown(defaultOverrideHoverContent), target: this.defaultOverrideIndicator.element, position: { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 7e9aab40f94..c0b28234b7a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isWeb, isWindows } from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; import { ExtensionToggleData } from '../common/preferences.js'; @@ -12,6 +13,7 @@ export interface ITOCEntry { order?: number; children?: ITOCEntry[]; settings?: Array; + hide?: boolean; } const defaultCommonlyUsedSettings: string[] = [ @@ -239,7 +241,8 @@ export const tocData: ITOCEntry = { { id: 'features/issueReporter', label: localize('issueReporter', 'Issue Reporter'), - settings: ['issueReporter.*'] + settings: ['issueReporter.*'], + hide: !isWeb } ] }, @@ -280,7 +283,8 @@ export const tocData: ITOCEntry = { { id: 'application/other', label: localize('other', "Other"), - settings: ['application.*'] + settings: ['application.*'], + hide: isWindows } ] }, @@ -319,3 +323,4 @@ knownTermMappings.set('power shell', 'PowerShell'); knownTermMappings.set('powershell', 'PowerShell'); knownTermMappings.set('javascript', 'JavaScript'); knownTermMappings.set('typescript', 'TypeScript'); +knownTermMappings.set('github', 'GitHub'); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index be9840d7bc1..2f49546c8e4 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -33,8 +33,9 @@ import { Disposable, DisposableStore, isDisposable, toDisposable } from '../../. import { isIOS } from '../../../../base/common/platform.js'; import { escapeRegExpCharacters } from '../../../../base/common/strings.js'; import { isDefined, isUndefinedOrNull } from '../../../../base/common/types.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { URI } from '../../../../base/common/uri.js'; import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { localize } from '../../../../nls.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -42,6 +43,7 @@ import { ConfigurationTarget, IConfigurationService, getLanguageTagSettingPlainK import { ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IListService, WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js'; @@ -55,23 +57,20 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { getIgnoredSettings } from '../../../../platform/userDataSync/common/settingsMerge.js'; import { IUserDataSyncEnablementService, getDefaultIgnoredSettings } from '../../../../platform/userDataSync/common/userDataSync.js'; +import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { ISetting, ISettingsGroup, SETTINGS_AUTHORITY, SettingValueType } from '../../../services/preferences/common/preferences.js'; +import { getInvalidTypeError } from '../../../services/preferences/common/preferencesValidation.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; +import { LANGUAGE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, compareTwoNullableNumbers } from '../common/preferences.js'; +import { settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js'; import { settingsMoreActionIcon } from './preferencesIcons.js'; import { SettingsTarget } from './preferencesWidgets.js'; import { ISettingOverrideClickEvent, SettingsTreeIndicatorsLabel, getIndicatorsLabelAriaLabel } from './settingsEditorSettingIndicators.js'; import { ITOCEntry } from './settingsLayout.js'; import { ISettingsEditorViewState, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement, inspectSetting, objectSettingSupportsRemoveDefaultValue, settingKeyToDisplayFormat } from './settingsTreeModels.js'; import { ExcludeSettingWidget, IBoolObjectDataItem, IIncludeExcludeDataItem, IListDataItem, IObjectDataItem, IObjectEnumOption, IObjectKeySuggester, IObjectValueSuggester, IncludeSettingWidget, ListSettingWidget, ObjectSettingCheckboxWidget, ObjectSettingDropdownWidget, ObjectValue, SettingListEvent } from './settingsWidgets.js'; -import { LANGUAGE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, compareTwoNullableNumbers } from '../common/preferences.js'; -import { settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js'; -import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; -import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { ISetting, ISettingsGroup, SETTINGS_AUTHORITY, SettingValueType } from '../../../services/preferences/common/preferences.js'; -import { getInvalidTypeError } from '../../../services/preferences/common/preferencesValidation.js'; -import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { URI } from '../../../../base/common/uri.js'; const $ = DOM.$; @@ -568,6 +567,7 @@ function _resolveSettingsTree(tocData: ITOCEntry, allSettings: Set[] | undefined; if (tocData.children) { children = tocData.children + .filter(child => child.hide !== true) .map(child => _resolveSettingsTree(child, allSettings, logService)) .filter(child => child.children?.length || child.settings?.length); } @@ -874,7 +874,9 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const descriptionElement = DOM.append(container, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - toDispose.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, () => localize('modified', "The setting has been configured in the current scope."))); + toDispose.add(this._hoverService.setupDelayedHover(modifiedIndicatorElement, { + content: localize('modified', "The setting has been configured in the current scope.") + })); const valueElement = DOM.append(container, $('.setting-item-value')); const controlElement = DOM.append(valueElement, $('div.setting-item-control')); @@ -961,7 +963,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const titleTooltip = setting.key + (element.isConfigured ? ' - Modified' : ''); template.categoryElement.textContent = element.displayCategory ? (element.displayCategory + ': ') : ''; - template.elementDisposables.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.categoryElement, titleTooltip)); + template.elementDisposables.add(this._hoverService.setupDelayedHover(template.categoryElement, { content: titleTooltip })); template.labelElement.text = element.displayLabel; template.labelElement.title = titleTooltip; @@ -1993,7 +1995,9 @@ class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRender const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); const descriptionElement = DOM.append(descriptionAndValueElement, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - toDispose.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, localize('modified', "The setting has been configured in the current scope."))); + toDispose.add(this._hoverService.setupDelayedHover(modifiedIndicatorElement, { + content: localize('modified', "The setting has been configured in the current scope.") + })); const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index d48eb562883..c6377173789 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -596,10 +596,15 @@ export class SettingsTreeModel implements IDisposable { if (tocEntry.settings) { const settingChildren = tocEntry.settings.map(s => this.createSettingsTreeSettingElement(s, element)); for (const child of settingChildren) { - if (!child.setting.deprecationMessage || child.isConfigured) { + if (!child.setting.deprecationMessage) { children.push(child); } else { - child.dispose(); + child.inspectSelf(); + if (child.isConfigured) { + children.push(child); + } else { + child.dispose(); + } } } } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 258984d755f..98702b1b4a7 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -8,30 +8,28 @@ import * as DOM from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; -import { Toggle, unthemedToggleStyles } from '../../../../base/browser/ui/toggle/toggle.js'; import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js'; import { SelectBox } from '../../../../base/browser/ui/selectBox/selectBox.js'; +import { Toggle, unthemedToggleStyles } from '../../../../base/browser/ui/toggle/toggle.js'; import { IAction } from '../../../../base/common/actions.js'; import { disposableTimeout } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { isIOS } from '../../../../base/common/platform.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import { isDefined, isUndefinedOrNull } from '../../../../base/common/types.js'; -import './media/settingsWidgets.css'; import { localize } from '../../../../nls.js'; import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { settingsDiscardIcon, settingsEditIcon, settingsRemoveIcon } from './preferencesIcons.js'; -import { settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js'; -import { defaultButtonStyles, getInputBoxStyle, getSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; +import { defaultButtonStyles, getInputBoxStyle, getSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { SettingValueType } from '../../../services/preferences/common/preferences.js'; +import { settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js'; +import './media/settingsWidgets.css'; +import { settingsDiscardIcon, settingsEditIcon, settingsRemoveIcon } from './preferencesIcons.js'; const $ = DOM.$; @@ -736,7 +734,7 @@ export class ListSettingWidget extends Abst : localize('listSiblingHintLabel', "List item `{0}` with sibling `${1}`", value.data, sibling); const { rowElement } = rowElementGroup; - this.listDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), rowElement, title)); + this.listDisposables.add(this.hoverService.setupDelayedHover(rowElement, { content: title })); rowElement.setAttribute('aria-label', title); } @@ -802,7 +800,7 @@ export class ExcludeSettingWidget extends ListSettingWidget('string'); private readonly nativeTabs = new ChangeObserver('boolean'); private readonly nativeFullScreen = new ChangeObserver('boolean'); private readonly clickThroughInactive = new ChangeObserver('boolean'); + private readonly controlsStyle = new ChangeObserver('string'); private readonly updateMode = new ChangeObserver('string'); private accessibilitySupport: 'on' | 'off' | 'auto' | undefined; private readonly workspaceTrustEnabled = new ChangeObserver('boolean'); @@ -61,6 +68,8 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private readonly enablePPEExtensionsGallery = new ChangeObserver('boolean'); private readonly restrictUNCAccess = new ChangeObserver('boolean'); private readonly accessibilityVerbosityDebug = new ChangeObserver('boolean'); + private readonly unifiedChatView = new ChangeObserver('boolean'); + private readonly telemetryDisableFeedback = new ChangeObserver('boolean'); constructor( @IHostService private readonly hostService: IHostService, @@ -117,6 +126,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // macOS: Click through (accept first mouse) processChanged(isMacintosh && this.clickThroughInactive.handleChange(config.window?.clickThroughInactive)); + // Windows/Linux: Window controls style + processChanged(!isMacintosh && this.controlsStyle.handleChange(config.window?.controlsStyle)); + // Update mode processChanged(this.updateMode.handleChange(config.update?.mode)); @@ -136,6 +148,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // Debug accessibility verbosity processChanged(this.accessibilityVerbosityDebug.handleChange(config?.accessibility?.verbosity?.debug)); + + // Unified chat view + processChanged(this.unifiedChatView.handleChange(config.chat?.experimental?.unifiedChatView)); } // Experiments @@ -144,6 +159,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // Profiles processChanged(this.productService.quality !== 'stable' && this.enablePPEExtensionsGallery.handleChange(config._extensionsGallery?.enablePPE)); + // Disable Feedback + processChanged(this.telemetryDisableFeedback.handleChange(config.telemetry?.disableFeedback)); + if (askToRelaunch && changed && this.hostService.hasFocus) { this.doConfirm( isNative ? diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index d8f7af344b4..a3cab6f679a 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -8,11 +8,11 @@ import { IRemoteAgentService, remoteConnectionLatencyMeasurer } from '../../../s import { RunOnceScheduler, retry } from '../../../../base/common/async.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { MenuId, IMenuService, MenuItemAction, MenuRegistry, registerAction2, Action2, SubmenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { MenuId, IMenuService, MenuItemAction, MenuRegistry, registerAction2, Action2, SubmenuItemAction, IMenu } from '../../../../platform/actions/common/actions.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from '../../../services/statusbar/browser/statusbar.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; -import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { Schemas } from '../../../../base/common/network.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; @@ -82,18 +82,16 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr private remoteStatusEntry: IStatusbarEntryAccessor | undefined; - private readonly legacyIndicatorMenu = this._register(this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService)); // to be removed once migration completed - private readonly remoteIndicatorMenu = this._register(this.menuService.createMenu(MenuId.StatusBarRemoteIndicatorMenu, this.contextKeyService)); + private readonly legacyIndicatorMenu: IMenu; // to be removed once migration completed + private readonly remoteIndicatorMenu: IMenu; private remoteMenuActionsGroups: ActionGroup[] | undefined; - private readonly remoteAuthority = this.environmentService.remoteAuthority; - private virtualWorkspaceLocation: { scheme: string; authority: string } | undefined = undefined; private connectionState: 'initializing' | 'connected' | 'reconnecting' | 'disconnected' | undefined = undefined; private connectionToken: string | undefined = undefined; - private readonly connectionStateContextKey = new RawContextKey<'' | 'initializing' | 'disconnected' | 'connected'>('remoteConnectionState', '').bindTo(this.contextKeyService); + private readonly connectionStateContextKey: IContextKey<'' | 'initializing' | 'disconnected' | 'connected'>; private networkState: 'online' | 'offline' | 'high-latency' | undefined = undefined; private measureNetworkConnectionLatencyScheduler: RunOnceScheduler | undefined = undefined; @@ -125,6 +123,10 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr return this._remoteExtensionMetadata; } + private get remoteAuthority(): string | undefined { + return this.environmentService.remoteAuthority; + } + private remoteMetadataInitialized: boolean = false; private readonly _onDidChangeEntries = this._register(new Emitter()); private readonly onDidChangeEntries: Event = this._onDidChangeEntries.event; @@ -152,6 +154,11 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr ) { super(); + this.legacyIndicatorMenu = this._register(this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService)); // to be removed once migration completed + this.remoteIndicatorMenu = this._register(this.menuService.createMenu(MenuId.StatusBarRemoteIndicatorMenu, this.contextKeyService)); + + this.connectionStateContextKey = new RawContextKey<'' | 'initializing' | 'disconnected' | 'connected'>('remoteConnectionState', '').bindTo(this.contextKeyService); + // Set initial connection state if (this.remoteAuthority) { this.connectionState = 'initializing'; diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index f7c3020d89a..f2bb13dad7f 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -26,6 +26,10 @@ import { PersistentConnection } from '../../../../platform/remote/common/remoteA import { IDownloadService } from '../../../../platform/download/common/download.js'; import { DownloadServiceChannel } from '../../../../platform/download/common/downloadIpc.js'; import { RemoteLoggerChannelClient } from '../../../../platform/log/common/logIpc.js'; +import { REMOTE_DEFAULT_IF_LOCAL_EXTENSIONS } from '../../../../platform/remote/common/remote.js'; + + +const EXTENSION_IDENTIFIER_PATTERN = '([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$'; export class LabelContribution implements IWorkbenchContribution { @@ -205,7 +209,7 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'object', markdownDescription: localize('remote.extensionKind', "Override the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions are run on the remote. By overriding an extension's default kind using this setting, you specify if that extension should be installed and enabled locally or remotely."), patternProperties: { - '([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$': { + [EXTENSION_IDENTIFIER_PATTERN]: { oneOf: [{ type: 'array', items: extensionKindSchema }, extensionKindSchema], default: ['ui'], }, @@ -355,6 +359,19 @@ Registry.as(ConfigurationExtensions.Configuration) enum: ['localhost', 'allInterfaces'], default: 'localhost', description: localize('remote.localPortHost', "Specifies the local host name that will be used for port forwarding.") + }, + [REMOTE_DEFAULT_IF_LOCAL_EXTENSIONS]: { + type: 'array', + markdownDescription: localize('remote.defaultExtensionsIfInstalledLocally.markdownDescription', 'List of extensions to install automatically on all remotes if already installed locally.'), + default: [ + 'GitHub.copilot', + 'GitHub.copilot-chat' + ], + items: { + type: 'string', + pattern: EXTENSION_IDENTIFIER_PATTERN, + patternErrorMessage: localize('remote.defaultExtensionsIfInstalledLocally.invalidFormat', 'Extension identifier must be in format "publisher.name".') + }, } } }); diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index a5d9ca5adde..c8a7faebab6 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -7,7 +7,7 @@ import { localize } from '../../../../nls.js'; import { basename } from '../../../../base/common/resources.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService } from '../common/scm.js'; +import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService, ISCMProvider } from '../common/scm.js'; import { IActivityService, NumberBadge } from '../../../services/activity/common/activity.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; @@ -30,49 +30,11 @@ const ActiveRepositoryContextKeys = { }; export class SCMActiveRepositoryController extends Disposable implements IWorkbenchContribution { - private readonly _countBadgeConfig = observableConfigValue<'all' | 'focused' | 'off'>('scm.countBadge', 'all', this.configurationService); - - private readonly _repositories = observableFromEvent(this, - Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), - () => this.scmService.repositories); - - private readonly _activeRepositoryHistoryItemRefName = derived(reader => { - const repository = this.scmViewService.activeRepository.read(reader); - const historyProvider = repository?.provider.historyProvider.read(reader); - const historyItemRef = historyProvider?.historyItemRef.read(reader); - - return historyItemRef?.name; - }); - - private readonly _countBadgeRepositories = derived(this, reader => { - switch (this._countBadgeConfig.read(reader)) { - case 'all': { - const repositories = this._repositories.read(reader); - return [...Iterable.map(repositories, r => ({ provider: r.provider, resourceCount: this._getRepositoryResourceCount(r) }))]; - } - case 'focused': { - const repository = this.scmViewService.activeRepository.read(reader); - return repository ? [{ provider: repository.provider, resourceCount: this._getRepositoryResourceCount(repository) }] : []; - } - case 'off': - return []; - default: - throw new Error('Invalid countBadge setting'); - } - }); - - private readonly _countBadge = derived(this, reader => { - let total = 0; - - for (const repository of this._countBadgeRepositories.read(reader)) { - const count = repository.provider.count?.read(reader); - const resourceCount = repository.resourceCount.read(reader); - - total = total + (count ?? resourceCount); - } - - return total; - }); + private readonly _repositories: IObservable>; + private readonly _activeRepositoryHistoryItemRefName: IObservable; + private readonly _countBadgeConfig: IObservable<'all' | 'focused' | 'off'>; + private readonly _countBadgeRepositories: IObservable }[]>; + private readonly _countBadge: IObservable; private _activeRepositoryNameContextKey: IContextKey; private _activeRepositoryBranchNameContextKey: IContextKey; @@ -96,8 +58,53 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe { name: 'activeRepositoryBranchName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryBranchName.key, } ]); + this._countBadgeConfig = observableConfigValue<'all' | 'focused' | 'off'>('scm.countBadge', 'all', this.configurationService); + + this._repositories = observableFromEvent(this, + Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), + () => this.scmService.repositories); + + this._activeRepositoryHistoryItemRefName = derived(reader => { + const repository = this.scmViewService.activeRepository.read(reader); + const historyProvider = repository?.provider.historyProvider.read(reader); + const historyItemRef = historyProvider?.historyItemRef.read(reader); + + return historyItemRef?.name; + }); + + this._countBadgeRepositories = derived(this, reader => { + switch (this._countBadgeConfig.read(reader)) { + case 'all': { + const repositories = this._repositories.read(reader); + return [...Iterable.map(repositories, r => ({ provider: r.provider, resourceCount: this._getRepositoryResourceCount(r) }))]; + } + case 'focused': { + const repository = this.scmViewService.activeRepository.read(reader); + return repository ? [{ provider: repository.provider, resourceCount: this._getRepositoryResourceCount(repository) }] : []; + } + case 'off': + return []; + default: + throw new Error('Invalid countBadge setting'); + } + }); + + this._countBadge = derived(this, reader => { + let total = 0; + + for (const repository of this._countBadgeRepositories.read(reader)) { + const count = repository.provider.count?.read(reader); + const resourceCount = repository.resourceCount.read(reader); + + total = total + (count ?? resourceCount); + } + + return total; + }); + this._register(autorunWithStore((reader, store) => { - this._updateActivityCountBadge(this._countBadge.read(reader), store); + const countBadge = this._countBadge.read(reader); + this._updateActivityCountBadge(countBadge, store); })); this._register(autorunWithStore((reader, store) => { @@ -165,7 +172,7 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe store.add(index === 0 ? this.statusbarService.addEntry(statusbarEntry, `status.scm.${index}`, MainThreadStatusBarAlignment.LEFT, 10000) : - this.statusbarService.addEntry(statusbarEntry, `status.scm.${index}`, MainThreadStatusBarAlignment.LEFT, { id: `status.scm.${index - 1}`, alignment: MainThreadStatusBarAlignment.RIGHT, compact: true }) + this.statusbarService.addEntry(statusbarEntry, `status.scm.${index}`, MainThreadStatusBarAlignment.LEFT, { location: { id: `status.scm.${index - 1}`, priority: 10000 }, alignment: MainThreadStatusBarAlignment.RIGHT, compact: true }) ); } } @@ -177,9 +184,7 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe } export class SCMActiveResourceContextKeyController extends Disposable implements IWorkbenchContribution { - private readonly _repositories = observableFromEvent(this, - Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), - () => this.scmService.repositories); + private readonly _repositories: IObservable>; private readonly _onDidRepositoryChange = new Emitter(); @@ -193,6 +198,10 @@ export class SCMActiveResourceContextKeyController extends Disposable implements const activeResourceHasChangesContextKey = new RawContextKey('scmActiveResourceHasChanges', false, localize('scmActiveResourceHasChanges', "Whether the active resource has changes")); const activeResourceRepositoryContextKey = new RawContextKey('scmActiveResourceRepository', undefined, localize('scmActiveResourceRepository', "The active resource's repository")); + this._repositories = observableFromEvent(this, + Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), + () => this.scmService.repositories); + this._store.add(autorunWithStore((reader, store) => { for (const repository of this._repositories.read(reader)) { store.add(Event.runAndSubscribe(repository.provider.onDidChangeResources, () => { diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts index 01c9eb2173c..ac52c163d58 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts @@ -22,7 +22,8 @@ import { IWorkbenchContribution } from '../../../common/contributions.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { autorun, autorunWithStore, observableFromEvent } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; +import { EditorInput } from '../../../common/editor/editorInput.js'; export const quickDiffDecorationCount = new RawContextKey('quickDiffDecorationCount', 0); @@ -188,8 +189,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben private enabled = false; private readonly quickDiffDecorationCount: IContextKey; - private readonly activeEditor = observableFromEvent(this, - this.editorService.onDidActiveEditorChange, () => this.editorService.activeEditor); + private readonly activeEditor: IObservable; // Resource URI -> Code Editor Id -> Decoration (Disposable) private readonly decorators = new ResourceMap>(); @@ -209,6 +209,9 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben this.quickDiffDecorationCount = quickDiffDecorationCount.bindTo(contextKeyService); + this.activeEditor = observableFromEvent(this, + this.editorService.onDidActiveEditorChange, () => this.editorService.activeEditor); + const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorations')); this._register(onDidChangeConfiguration(this.onDidChangeConfiguration, this)); this.onDidChangeConfiguration(); diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 64d04fc0cbc..501e99edf7e 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -318,7 +318,7 @@ class HistoryItemRenderer implements ITreeRenderer('scm.graph.badges', 'filter', this._configurationService); + private readonly _badgesConfig: IObservable<'all' | 'filter'>; constructor( private readonly hoverDelegate: IHoverDelegate, @@ -328,7 +328,9 @@ class HistoryItemRenderer implements ITreeRenderer('scm.graph.badges', 'filter', this._configurationService); + } renderTemplate(container: HTMLElement): HistoryItemTemplate { // hack @@ -749,36 +751,13 @@ type RepositoryState = { }; class SCMHistoryViewModel extends Disposable { - - private readonly _closedRepository = observableFromEvent( - this, - this._scmService.onDidRemoveRepository, - repository => repository); - - private readonly _firstRepository = this._scmService.repositoryCount > 0 ? - constObservable(Iterable.first(this._scmService.repositories)) : - observableFromEvent( - this, - Event.once(this._scmService.onDidAddRepository), - repository => repository - ); - - private readonly _selectedRepository = observableValue<'auto' | ISCMRepository>(this, 'auto'); - - private readonly _graphRepository = derived(reader => { - const selectedRepository = this._selectedRepository.read(reader); - if (selectedRepository !== 'auto') { - return selectedRepository; - } - - return this._scmViewService.activeRepository.read(reader); - }); - /** * The active | selected repository takes precedence over the first repository when the observable * values are updated in the same transaction (or during the initial read of the observable value). */ - readonly repository = latestChangedValue(this, [this._firstRepository, this._graphRepository]); + readonly repository: IObservable; + private readonly _selectedRepository = observableValue<'auto' | ISCMRepository>(this, 'auto'); + readonly onDidChangeHistoryItemsFilter = observableSignal(this); readonly isViewModelEmpty = observableValue(this, false); @@ -804,9 +783,30 @@ class SCMHistoryViewModel extends Disposable { this._scmHistoryItemCountCtx = ContextKeys.SCMHistoryItemCount.bindTo(this._contextKeyService); + const firstRepository = this._scmService.repositoryCount > 0 + ? constObservable(Iterable.first(this._scmService.repositories)) + : observableFromEvent(this, + Event.once(this._scmService.onDidAddRepository), + repository => repository); + + const graphRepository = derived(reader => { + const selectedRepository = this._selectedRepository.read(reader); + if (selectedRepository !== 'auto') { + return selectedRepository; + } + + return this._scmViewService.activeRepository.read(reader); + }); + + this.repository = latestChangedValue(this, [firstRepository, graphRepository]); + + const closedRepository = observableFromEvent(this, + this._scmService.onDidRemoveRepository, + repository => repository); + // Closed repository cleanup this._register(autorun(reader => { - const repository = this._closedRepository.read(reader); + const repository = closedRepository.read(reader); if (!repository) { return; } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 52af3ec95aa..3e4d5f73507 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -18,9 +18,10 @@ import { binarySearch } from '../../../../base/common/arrays.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { derivedObservableWithCache, derivedOpts, latestChangedValue, observableFromEventOpts } from '../../../../base/common/observable.js'; +import { derivedObservableWithCache, derivedOpts, IObservable, latestChangedValue, observableFromEventOpts } from '../../../../base/common/observable.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; +import { EditorInput } from '../../../common/editor/editorInput.js'; function getProviderStorageKey(provider: ISCMProvider): string { return `${provider.contextValue}:${provider.label}${provider.rootUri ? `:${provider.rootUri.toString()}` : ''}`; @@ -157,49 +158,16 @@ export class SCMViewService implements ISCMViewService { private _onDidFocusRepository = new Emitter(); readonly onDidFocusRepository = this._onDidFocusRepository.event; - private readonly _focusedRepository = observableFromEventOpts( - { - owner: this, - equalsFn: () => false - }, this.onDidFocusRepository, - () => this.focusedRepository); - - private readonly _activeEditor = observableFromEventOpts( - { - owner: this, - equalsFn: () => false - }, this.editorService.onDidActiveEditorChange, - () => this.editorService.activeEditor); - - private readonly _activeEditorRepository = derivedObservableWithCache(this, - (reader, lastValue) => { - const activeEditor = this._activeEditor.read(reader); - const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor); - if (!activeResource) { - return lastValue; - } - - const repository = this.scmService.getRepository(activeResource); - if (!repository) { - return lastValue; - } - - return Object.create(repository); - }); + readonly activeRepository: IObservable; + private readonly _activeEditorObs: IObservable; + private readonly _activeEditorRepositoryObs: IObservable; /** * The focused repository takes precedence over the active editor repository when the observable * values are updated in the same transaction (or during the initial read of the observable value). - */ - private readonly _activeRepository = latestChangedValue(this, [this._activeEditorRepository, this._focusedRepository]); - - /** - * Derived with a custom equality function - */ - readonly activeRepository = derivedOpts({ - owner: this, - equalsFn: (r1, r2) => r1?.id === r2?.id - }, reader => this._activeRepository.read(reader)); + */ + private readonly _activeRepositoryObs: IObservable; + private readonly _focusedRepositoryObs: IObservable; private _repositoriesSortKey: ISCMRepositorySortKey; private _sortKeyContextKey: IContextKey; @@ -216,6 +184,43 @@ export class SCMViewService implements ISCMViewService { ) { this.menus = instantiationService.createInstance(SCMMenus); + this._focusedRepositoryObs = observableFromEventOpts( + { + owner: this, + equalsFn: () => false + }, this.onDidFocusRepository, + () => this.focusedRepository); + + this._activeEditorObs = observableFromEventOpts( + { + owner: this, + equalsFn: () => false + }, this.editorService.onDidActiveEditorChange, + () => this.editorService.activeEditor); + + this._activeEditorRepositoryObs = derivedObservableWithCache(this, + (reader, lastValue) => { + const activeEditor = this._activeEditorObs.read(reader); + const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor); + if (!activeResource) { + return lastValue; + } + + const repository = this.scmService.getRepository(activeResource); + if (!repository) { + return lastValue; + } + + return Object.create(repository); + }); + + this._activeRepositoryObs = latestChangedValue(this, [this._activeEditorRepositoryObs, this._focusedRepositoryObs]); + + this.activeRepository = derivedOpts({ + owner: this, + equalsFn: (r1, r2) => r1?.id === r2?.id + }, reader => this._activeRepositoryObs.read(reader)); + try { this.previousState = JSON.parse(storageService.get('scm:view:visibleRepositories', StorageScope.WORKSPACE, '')); } catch { diff --git a/src/vs/workbench/contrib/scm/browser/workingSet.ts b/src/vs/workbench/contrib/scm/browser/workingSet.ts index 1185f8de20a..1f53f8d298b 100644 --- a/src/vs/workbench/contrib/scm/browser/workingSet.ts +++ b/src/vs/workbench/contrib/scm/browser/workingSet.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, derived } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, derived, IObservable } from '../../../../base/common/observable.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; @@ -28,8 +28,8 @@ interface ISCMRepositoryWorkingSet { export class SCMWorkingSetController extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.scmWorkingSets'; + private _enabledConfig: IObservable; private _workingSets!: Map; - private _enabledConfig = observableConfigValue('scm.workingSets.enabled', false, this.configurationService); private readonly _repositoryDisposables = new DisposableMap(); @@ -42,6 +42,8 @@ export class SCMWorkingSetController extends Disposable implements IWorkbenchCon ) { super(); + this._enabledConfig = observableConfigValue('scm.workingSets.enabled', false, this.configurationService); + this._store.add(autorunWithStore((reader, store) => { if (!this._enabledConfig.read(reader)) { this.storageService.remove('scm.workingSets', StorageScope.WORKSPACE); diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index 55cd69561f0..60dd34b399e 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -14,7 +14,10 @@ import { LineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js import { IChange } from '../../../../editor/common/diff/legacyLinesDiffComputer.js'; import { IColorTheme } from '../../../../platform/theme/common/themeService.js'; import { Color } from '../../../../base/common/color.js'; -import { editorErrorForeground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; +import { + darken, editorBackground, editorForeground, listInactiveSelectionBackground, opaque, + editorErrorForeground, registerColor, transparent +} from '../../../../platform/theme/common/colorRegistry.js'; export const IQuickDiffService = createDecorator('quickDiff'); @@ -45,6 +48,12 @@ export const overviewRulerAddedForeground = registerColor('editorOverviewRuler.a export const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.deletedForeground', transparent(editorGutterDeletedBackground, 0.6), nls.localize('overviewRulerDeletedForeground', 'Overview ruler marker color for deleted content.')); +export const editorGutterItemGlyphForeground = registerColor('editorGutter.itemGlyphForeground', + { dark: editorForeground, light: editorForeground, hcDark: Color.black, hcLight: Color.white }, + nls.localize('editorGutterItemGlyphForeground', 'Editor gutter decoration color for gutter item glyphs.') +); +export const editorGutterItemBackground = registerColor('editorGutter.itemBackground', { dark: opaque(listInactiveSelectionBackground, editorBackground), light: darken(opaque(listInactiveSelectionBackground, editorBackground), .05), hcDark: Color.white, hcLight: Color.black }, nls.localize('editorGutterItemBackground', 'Editor gutter decoration color for gutter item background. This color should be opaque.')); + export interface QuickDiffProvider { label: string; rootUri: URI | undefined; diff --git a/src/vs/workbench/contrib/search/browser/AISearch/aiSearchModel.ts b/src/vs/workbench/contrib/search/browser/AISearch/aiSearchModel.ts index 3a21a6db752..df9d8a5dccd 100644 --- a/src/vs/workbench/contrib/search/browser/AISearch/aiSearchModel.ts +++ b/src/vs/workbench/contrib/search/browser/AISearch/aiSearchModel.ts @@ -295,6 +295,7 @@ export class AIFolderMatchWorkspaceRootImpl extends Disposable implements ISearc private disposeMatches(): void { [...this._fileMatches.values()].forEach((fileMatch: ISearchTreeFileMatch) => fileMatch.dispose()); [...this._unDisposedFileMatches.values()].forEach((fileMatch: ISearchTreeFileMatch) => fileMatch.dispose()); + this._fileMatches.clear(); } override dispose(): void { diff --git a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts index ea1260b104f..54d7b8f24d8 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts @@ -24,6 +24,7 @@ import { category, getElementsToOperateOn, getSearchView, shouldRefocus } from ' import { equals } from '../../../../base/common/arrays.js'; import { arrayContainsElementOrParent, RenderableMatch, ISearchResult, isSearchTreeFileMatch, isSearchTreeFolderMatch, isSearchTreeMatch, isSearchResult, isTextSearchHeading } from './searchTreeModel/searchTreeCommon.js'; import { MatchInNotebook } from './notebookSearch/notebookSearchModel.js'; +import { AITextSearchHeadingImpl } from './AISearch/aiSearchModel.js'; //#region Interfaces @@ -375,10 +376,18 @@ export async function getElementToFocusAfterRemoved(viewer: WorkbenchCompressibl while (!!navigator.next() && (!isSearchTreeFolderMatch(navigator.current()) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { } } else if (isSearchTreeFileMatch(element)) { while (!!navigator.next() && (!isSearchTreeFileMatch(navigator.current()) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { + // Never expand AI search results by default + if (navigator.current() instanceof AITextSearchHeadingImpl) { + return navigator.current(); + } await viewer.expand(navigator.current()); } } else { while (navigator.next() && (!isSearchTreeMatch(navigator.current()) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { + // Never expand AI search results by default + if (navigator.current() instanceof AITextSearchHeadingImpl) { + return navigator.current(); + } await viewer.expand(navigator.current()); } } diff --git a/src/vs/workbench/contrib/search/browser/searchMessage.ts b/src/vs/workbench/contrib/search/browser/searchMessage.ts index 37a03f28a89..ea8990aad3e 100644 --- a/src/vs/workbench/contrib/search/browser/searchMessage.ts +++ b/src/vs/workbench/contrib/search/browser/searchMessage.ts @@ -10,7 +10,7 @@ import { parseLinkedText } from '../../../../base/common/linkedText.js'; import Severity from '../../../../base/common/severity.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { TextSearchCompleteMessage, TextSearchCompleteMessageType } from '../../../services/search/common/searchExtTypes.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { Schemas } from '../../../../base/common/network.js'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 2ef0de41b46..06c660a0325 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -201,7 +201,7 @@ export class SearchEditor extends AbstractTextCodeEditor ariaLabel: localize('label.includes', 'Search Include Patterns'), inputBoxStyles: searchEditorInputboxStyles })); - this.inputPatternIncludes.onSubmit(triggeredOnType => this.triggerSearch({ resetCursor: false, delay: triggeredOnType ? this.searchConfig.searchOnTypeDebouncePeriod : 0 })); + this._register(this.inputPatternIncludes.onSubmit(triggeredOnType => this.triggerSearch({ resetCursor: false, delay: triggeredOnType ? this.searchConfig.searchOnTypeDebouncePeriod : 0 }))); this._register(this.inputPatternIncludes.onChangeSearchInEditorsBox(() => this.triggerSearch())); // Excludes diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index 8529260f59a..45d4b02a122 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -7,7 +7,7 @@ import { localize } from '../../../../nls.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { DeferredPromise } from '../../../../base/common/async.js'; @@ -58,11 +58,11 @@ export class SpeechService extends Disposable implements ISpeechService { private readonly providers = new Map(); private readonly providerDescriptors = new Map(); - private readonly hasSpeechProviderContext = HasSpeechProvider.bindTo(this.contextKeyService); + private readonly hasSpeechProviderContext: IContextKey; constructor( @ILogService private readonly logService: ILogService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -70,6 +70,10 @@ export class SpeechService extends Disposable implements ISpeechService { ) { super(); + this.hasSpeechProviderContext = HasSpeechProvider.bindTo(contextKeyService); + this.textToSpeechInProgress = TextToSpeechInProgress.bindTo(contextKeyService); + this.speechToTextInProgress = SpeechToTextInProgress.bindTo(contextKeyService); + this.handleAndRegisterSpeechExtensions(); } @@ -136,7 +140,7 @@ export class SpeechService extends Disposable implements ISpeechService { private activeSpeechToTextSessions = 0; get hasActiveSpeechToTextSession() { return this.activeSpeechToTextSessions > 0; } - private readonly speechToTextInProgress = SpeechToTextInProgress.bindTo(this.contextKeyService); + private readonly speechToTextInProgress: IContextKey; async createSpeechToTextSession(token: CancellationToken, context: string = 'speech'): Promise { const provider = await this.getProvider(); @@ -249,7 +253,7 @@ export class SpeechService extends Disposable implements ISpeechService { private activeTextToSpeechSessions = 0; get hasActiveTextToSpeechSession() { return this.activeTextToSpeechSessions > 0; } - private readonly textToSpeechInProgress = TextToSpeechInProgress.bindTo(this.contextKeyService); + private readonly textToSpeechInProgress: IContextKey; async createTextToSpeechSession(token: CancellationToken, context: string = 'speech'): Promise { const provider = await this.getProvider(); diff --git a/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts b/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts index ba0e35ef849..8d5e219219e 100644 --- a/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts @@ -15,6 +15,7 @@ import { Severity, INotificationService, NotificationPriority } from '../../../. import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { URI } from '../../../../base/common/uri.js'; import { platform } from '../../../../base/common/process.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; const PROBABILITY = 0.15; const SESSION_COUNT_KEY = 'nps/sessionCount'; @@ -29,9 +30,10 @@ class NPSContribution implements IWorkbenchContribution { @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IOpenerService openerService: IOpenerService, - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService ) { - if (!productService.npsSurveyUrl) { + if (!productService.npsSurveyUrl || configurationService.getValue('telemetry.disableFeedback')) { return; } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index d6a12341d75..4672ffa2c9b 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -47,8 +47,8 @@ import { ITextFileService } from '../../../services/textfile/common/textfiles.js import { ITerminalGroupService, ITerminalService } from '../../terminal/browser/terminal.js'; import { ITerminalProfileResolverService } from '../../terminal/common/terminal.js'; -import { ConfiguringTask, ContributedTask, CustomTask, ExecutionEngine, InMemoryTask, ITaskEvent, ITaskIdentifier, ITaskSet, JsonSchemaVersion, KeyedTaskIdentifier, RuntimeType, Task, TASK_RUNNING_STATE, TaskDefinition, TaskEventKind, TaskGroup, TaskRunSource, TaskSettingId, TaskSorter, TaskSourceKind, TasksSchemaProperties, USER_TASKS_GROUP_KEY } from '../common/tasks.js'; -import { CustomExecutionSupportedContext, ICustomizationProperties, IProblemMatcherRunOptions, ITaskFilter, ITaskProvider, ITaskService, IWorkspaceFolderTaskResult, ProcessExecutionSupportedContext, ServerlessWebContext, ShellExecutionSupportedContext, TaskCommandsRegistered, TaskExecutionSupportedContext } from '../common/taskService.js'; +import { ConfiguringTask, ContributedTask, CustomTask, ExecutionEngine, InMemoryTask, ITaskEvent, ITaskIdentifier, ITaskSet, JsonSchemaVersion, KeyedTaskIdentifier, RuntimeType, Task, TASK_RUNNING_STATE, TaskDefinition, TaskGroup, TaskRunSource, TaskSettingId, TaskSorter, TaskSourceKind, TasksSchemaProperties, USER_TASKS_GROUP_KEY, TaskEventKind } from '../common/tasks.js'; +import { CustomExecutionSupportedContext, ICustomizationProperties, IProblemMatcherRunOptions, ITaskFilter, ITaskProvider, ITaskService, ITaskTerminalStatus, IWorkspaceFolderTaskResult, ProcessExecutionSupportedContext, ServerlessWebContext, ShellExecutionSupportedContext, TaskCommandsRegistered, TaskExecutionSupportedContext } from '../common/taskService.js'; import { ITaskExecuteResult, ITaskResolver, ITaskSummary, ITaskSystem, ITaskSystemInfo, ITaskTerminateResponse, TaskError, TaskErrors, TaskExecuteKind } from '../common/taskSystem.js'; import { getTemplates as getTaskTemplates } from '../common/taskTemplates.js'; @@ -237,6 +237,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer public get isReconnected(): boolean { return this._tasksReconnected; } private _onDidChangeTaskProviders = this._register(new Emitter()); public onDidChangeTaskProviders = this._onDidChangeTaskProviders.event; + private _onDidChangeTaskTerminalStatus: Emitter = new Emitter(); + public readonly onDidChangeTaskTerminalStatus: Event = this._onDidChangeTaskTerminalStatus.event; private _activatedTaskProviders: Set = new Set(); diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index aab10e08871..8629aa38b0f 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -20,7 +20,7 @@ import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatus import { IOutputChannelRegistry, Extensions as OutputExt } from '../../../services/output/common/output.js'; -import { ITaskEvent, TaskEventKind, TaskGroup, TaskSettingId, TASKS_CATEGORY, TASK_RUNNING_STATE, TASK_TERMINAL_ACTIVE } from '../common/tasks.js'; +import { ITaskEvent, TaskGroup, TaskSettingId, TASKS_CATEGORY, TASK_RUNNING_STATE, TASK_TERMINAL_ACTIVE, TaskEventKind } from '../common/tasks.js'; import { ITaskService, TaskCommandsRegistered, TaskExecutionSupportedContext } from '../common/taskService.js'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; @@ -142,7 +142,7 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench }; if (!this._runningTasksStatusItem) { - this._runningTasksStatusItem = this._statusbarService.addEntry(itemProps, 'status.runningTasks', StatusbarAlignment.LEFT, 49 /* Medium Priority, next to Markers */); + this._runningTasksStatusItem = this._statusbarService.addEntry(itemProps, 'status.runningTasks', StatusbarAlignment.LEFT, { location: { id: 'status.problems', priority: 50 }, alignment: StatusbarAlignment.RIGHT }); } else { this._runningTasksStatusItem.update(itemProps); } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 61d996724fe..31f4e203f54 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -833,15 +833,18 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { eventCounter++; this._busyTasks[mapKey] = task; this._fireTaskEvent(TaskEvent.general(TaskEventKind.Active, task, terminal?.instanceId)); + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherStarted, task, terminal?.instanceId)); } else if (event.kind === ProblemCollectorEventKind.BackgroundProcessingEnds) { eventCounter--; if (this._busyTasks[mapKey]) { delete this._busyTasks[mapKey]; } this._fireTaskEvent(TaskEvent.general(TaskEventKind.Inactive, task, terminal?.instanceId)); + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherEnded, task, terminal?.instanceId)); if (eventCounter === 0) { if ((watchingProblemMatcher.numberOfMatches > 0) && watchingProblemMatcher.maxMarkerSeverity && (watchingProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error)) { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherFoundErrors, task, terminal?.instanceId)); const reveal = task.command.presentation!.reveal; const revealProblems = task.command.presentation!.revealProblems; if (revealProblems === RevealProblemKind.OnProblem) { @@ -981,7 +984,16 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { const problemMatchers = await this._resolveMatchers(resolver, task.configurationProperties.problemMatchers); const startStopProblemMatcher = new StartStopProblemCollector(problemMatchers, this._markerService, this._modelService, ProblemHandlingStrategy.Clean, this._fileService); this._terminalStatusManager.addTerminal(task, terminal, startStopProblemMatcher); - + startStopProblemMatcher.onDidStateChange((event) => { + if (event.kind === ProblemCollectorEventKind.BackgroundProcessingBegins) { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherStarted, task, terminal?.instanceId)); + } else if (event.kind === ProblemCollectorEventKind.BackgroundProcessingEnds) { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherEnded, task, terminal?.instanceId)); + if (startStopProblemMatcher.numberOfMatches) { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherFoundErrors, task, terminal?.instanceId)); + } + } + }); let processStartedSignaled = false; terminal.processReady.then(() => { if (!processStartedSignaled) { @@ -1044,6 +1056,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { delete this._busyTasks[mapKey]; } this._fireTaskEvent(TaskEvent.general(TaskEventKind.Inactive, task, terminal?.instanceId)); + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherEnded, task, terminal?.instanceId)); this._fireTaskEvent(TaskEvent.general(TaskEventKind.End, task, terminal?.instanceId)); resolve({ exitCode: exitCode ?? undefined }); }); diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index ac2f0c34c3b..ec687197dee 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -108,3 +108,8 @@ export interface ITaskService { extensionCallbackTaskComplete(task: Task, result: number | undefined): Promise; } + +export interface ITaskTerminalStatus { + terminalId: number; + status: string; +} diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index c78cb6d4d77..3c01d029ade 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -1102,18 +1102,6 @@ export class TaskSorter { } } -export const enum TaskEventKind { - DependsOnStarted = 'dependsOnStarted', - AcquiredInput = 'acquiredInput', - Start = 'start', - ProcessStarted = 'processStarted', - Active = 'active', - Inactive = 'inactive', - Changed = 'changed', - Terminated = 'terminated', - ProcessEnded = 'processEnded', - End = 'end' -} export const enum TaskRunType { @@ -1125,6 +1113,49 @@ export interface ITaskChangedEvent { kind: TaskEventKind.Changed; } + + +export enum TaskEventKind { + /** Indicates that a task's properties or configuration have changed */ + Changed = 'changed', + + /** Indicates that a task has begun executing */ + ProcessStarted = 'processStarted', + + /** Indicates that a task process has completed */ + ProcessEnded = 'processEnded', + + /** Indicates that a task was terminated, either by user action or by the system */ + Terminated = 'terminated', + + /** Indicates that a task has started running */ + Start = 'start', + + /** Indicates that a task has acquired all needed input/variables to execute */ + AcquiredInput = 'acquiredInput', + + /** Indicates that a dependent task has started */ + DependsOnStarted = 'dependsOnStarted', + + /** Indicates that a task is actively running/processing */ + Active = 'active', + + /** Indicates that a task is paused/waiting but not complete */ + Inactive = 'inactive', + + /** Indicates that a task has completed fully */ + End = 'end', + + /** Indicates that a task's problem matcher has started */ + ProblemMatcherStarted = 'problemMatcherStarted', + + /** Indicates that a task's problem matcher has ended */ + ProblemMatcherEnded = 'problemMatcherEnded', + + /** Indicates that a task's problem matcher has found errors */ + ProblemMatcherFoundErrors = 'problemMatcherFoundErrors' +} + interface ITaskCommon { taskId: string; runType: TaskRunType; @@ -1158,7 +1189,7 @@ export interface ITaskStartedEvent extends ITaskCommon { } export interface ITaskGeneralEvent extends ITaskCommon { - kind: TaskEventKind.AcquiredInput | TaskEventKind.DependsOnStarted | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.End; + kind: TaskEventKind.AcquiredInput | TaskEventKind.DependsOnStarted | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.End | TaskEventKind.ProblemMatcherEnded | TaskEventKind.ProblemMatcherStarted | TaskEventKind.ProblemMatcherFoundErrors; terminalId: number | undefined; } @@ -1224,7 +1255,7 @@ export namespace TaskEvent { }; } - export function general(kind: TaskEventKind.AcquiredInput | TaskEventKind.DependsOnStarted | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.End, task: Task, terminalId?: number): ITaskGeneralEvent { + export function general(kind: TaskEventKind.AcquiredInput | TaskEventKind.DependsOnStarted | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.End | TaskEventKind.ProblemMatcherEnded | TaskEventKind.ProblemMatcherStarted | TaskEventKind.ProblemMatcherFoundErrors, task: Task, terminalId?: number): ITaskGeneralEvent { return { ...common(task), kind, diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 06e08fcd789..15ffde581b3 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -154,15 +154,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr private onTextFileModelResolved(e: ITextFileResolveEvent): void { const settingsType = this.getTypeIfSettings(e.model.resource); - if (settingsType) { - type SettingsReadClassification = { - owner: 'isidorn'; - settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the settings file that was read.' }; - comment: 'Track when a settings file was read, for example from an editor.'; - }; - - this.telemetryService.publicLog2<{ settingsType: string }, SettingsReadClassification>('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data - } else { + if (!settingsType) { type FileGetClassification = { owner: 'isidorn'; comment: 'Track when a file was read, for example from an editor.'; @@ -174,14 +166,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr private onTextFileModelSaved(e: ITextFileSaveEvent): void { const settingsType = this.getTypeIfSettings(e.model.resource); - if (settingsType) { - type SettingsWrittenClassification = { - owner: 'isidorn'; - settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the settings file that was written to.' }; - comment: 'Track when a settings file was written to, for example from an editor.'; - }; - this.telemetryService.publicLog2<{ settingsType: string }, SettingsWrittenClassification>('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data - } else { + if (!settingsType) { type FilePutClassfication = { owner: 'isidorn'; comment: 'Track when a file was written to, for example from an editor.'; diff --git a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts index 1ec81c70b9f..9da2be1b5f2 100644 --- a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts +++ b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts @@ -87,7 +87,8 @@ export class EnvironmentVariableInfoChangesActive implements IEnvironmentVariabl return { id: TerminalStatus.EnvironmentVariableInfoChangesActive, severity: Severity.Info, - tooltip: this._getInfo(scope), + tooltip: undefined, // The action is present when details aren't shown + detailedTooltip: this._getInfo(scope), hoverActions: this._getActions(scope) }; } diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts index f256d72e4c4..03c08c8151d 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts @@ -190,6 +190,7 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack type: shellLaunchConfig.type, isFeatureTerminal: shellLaunchConfig.isFeatureTerminal, tabActions: shellLaunchConfig.tabActions, + shellIntegrationEnvironmentReporting: shellLaunchConfig.shellIntegrationEnvironmentReporting, }; const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index cee30a6d28b..4a444c49fcb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getFontSnippets } from '../../../../base/browser/fonts.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Schemas } from '../../../../base/common/network.js'; import { isIOS, isWindows } from '../../../../base/common/platform.js'; @@ -66,7 +67,7 @@ registerWorkbenchContribution2(RemoteTerminalBackendContribution.ID, RemoteTermi // Register configurations registerTerminalPlatformConfiguration(); -registerTerminalConfiguration(); +registerTerminalConfiguration(getFontSnippets); // Register editor/dnd contributions Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(TerminalEditorInput.ID, TerminalInputSerializer); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 664eea28908..6008c2b6dd7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -904,7 +904,7 @@ export function registerTerminalActions() { registerActiveInstanceAction({ id: TerminalCommandId.SelectToPreviousCommand, - title: localize2('workbench.action.terminal.selectToPreviousCommand', 'Select To Previous Command'), + title: localize2('workbench.action.terminal.selectToPreviousCommand', 'Select to Previous Command'), keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow, when: TerminalContextKeys.focus, @@ -919,7 +919,7 @@ export function registerTerminalActions() { registerActiveInstanceAction({ id: TerminalCommandId.SelectToNextCommand, - title: localize2('workbench.action.terminal.selectToNextCommand', 'Select To Next Command'), + title: localize2('workbench.action.terminal.selectToNextCommand', 'Select to Next Command'), keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow, when: TerminalContextKeys.focus, @@ -934,7 +934,7 @@ export function registerTerminalActions() { registerActiveXtermAction({ id: TerminalCommandId.SelectToPreviousLine, - title: localize2('workbench.action.terminal.selectToPreviousLine', 'Select To Previous Line'), + title: localize2('workbench.action.terminal.selectToPreviousLine', 'Select to Previous Line'), precondition: sharedWhenClause.terminalAvailable, run: async (xterm, _, instance) => { xterm.markTracker.selectToPreviousLine(); @@ -945,7 +945,7 @@ export function registerTerminalActions() { registerActiveXtermAction({ id: TerminalCommandId.SelectToNextLine, - title: localize2('workbench.action.terminal.selectToNextLine', 'Select To Next Line'), + title: localize2('workbench.action.terminal.selectToNextLine', 'Select to Next Line'), precondition: sharedWhenClause.terminalAvailable, run: async (xterm, _, instance) => { xterm.markTracker.selectToNextLine(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts b/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts index 4e295a44f37..f5f78bd178f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts @@ -58,7 +58,7 @@ export class TerminalIconPicker extends Disposable { this._iconSelectBox.dispose(); })); this._iconSelectBox.clearInput(); - const hoverWidget = this._hoverService.showHover({ + const hoverWidget = this._hoverService.showInstantHover({ content: this._iconSelectBox.domNode, target: getActiveDocument().body, position: { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index e0a7854c29d..7893790b4c1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -90,6 +90,7 @@ import type { IMenu } from '../../../../platform/actions/common/actions.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { TerminalContribCommandId } from '../terminalContribExports.js'; import type { IProgressState } from '@xterm/addon-progress'; +import { refreshShellIntegrationInfoStatus } from './terminalTooltip.js'; const enum Constants { /** @@ -846,6 +847,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Init winpty compat and link handler after process creation as they rely on the // underlying process OS this._register(this._processManager.onProcessReady(async (processTraits) => { + // Respond to DA1 with basic conformance. Note that including this is required to avoid + // a long delay in conpty 1.22+ where it waits for the response. + // Reference: https://github.com/microsoft/terminal/blob/3760caed97fa9140a40777a8fbc1c95785e6d2ab/src/terminal/adapter/adaptDispatch.cpp#L1471-L1495 + if (processTraits?.windowsPty?.backend === 'conpty') { + this._register(xterm.raw.parser.registerCsiHandler({ final: 'c' }, params => { + if (params.length === 0 || params.length === 1 && params[0] === 0) { + this._processManager.write('\x1b[?61;4c'); + return true; + } + return false; + })); + } if (this._processManager.os) { lineDataEventAddon.setOperatingSystem(this._processManager.os); } @@ -860,6 +873,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { })); this._register(xterm.onDidChangeProgress(() => this._labelComputer?.refreshLabel(this))); + // Register and update the terminal's shell integration status + this._register(Event.runAndSubscribe(xterm.shellIntegration.onDidChangeSeenSequences, () => { + if (xterm.shellIntegration.seenSequences.size > 0) { + refreshShellIntegrationInfoStatus(this); + } + })); + // Set up updating of the process cwd on key press, this is only needed when the cwd // detection capability has not been registered if (!this.capabilities.has(TerminalCapability.CwdDetection)) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts index ecc8d38258b..bdbbe66141e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts @@ -24,6 +24,7 @@ export const enum TerminalStatus { Disconnected = 'disconnected', RelaunchNeeded = 'relaunch-needed', EnvironmentVariableInfoChangesActive = 'env-var-info-changes-active', + ShellIntegrationInfo = 'shell-integration-info', ShellIntegrationAttentionNeeded = 'shell-integration-attention-needed' } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index 872391084be..3213dd89247 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -458,8 +458,8 @@ export class TerminalTabbedView extends Disposable { if (!instance) { return; } - this._hoverService.showHover({ - ...getInstanceHoverInfo(instance), + this._hoverService.showInstantHover({ + ...getInstanceHoverInfo(instance, this._storageService), target: this._terminalContainer, trapFocus: true }, true); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 5d8140d604b..bf8e35c81b3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -53,6 +53,8 @@ import { TerminalContextActionRunner } from './terminalContextMenu.js'; import type { IHoverAction } from '../../../../base/browser/ui/hover/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js'; +import { TerminalStorageKeys } from '../common/terminalStorageKeys.js'; const $ = DOM.$; @@ -82,6 +84,7 @@ export class TerminalTabList extends WorkbenchList { @IInstantiationService instantiationService: IInstantiationService, @IDecorationsService decorationsService: IDecorationsService, @IThemeService private readonly _themeService: IThemeService, + @IStorageService private readonly _storageService: IStorageService, @ILifecycleService lifecycleService: ILifecycleService, @IHoverService private readonly _hoverService: IHoverService, ) { @@ -128,7 +131,8 @@ export class TerminalTabList extends WorkbenchList { this.reveal(i); } this.refresh(); - }) + }), + this._storageService.onDidChangeValue(StorageScope.APPLICATION, TerminalStorageKeys.TabsShowDetailed, this.disposables)(() => this.refresh()), ]; // Dispose of instance listeners on shutdown to avoid extra work and so tabs don't disappear @@ -229,8 +233,8 @@ export class TerminalTabList extends WorkbenchList { return; } - this._hoverService.showHover({ - ...getInstanceHoverInfo(instance), + this._hoverService.showInstantHover({ + ...getInstanceHoverInfo(instance, this._storageService), target: this.getHTMLElement(), trapFocus: true }, true); @@ -257,6 +261,7 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer 0) { - shellIntegrationString += `${markdown ? '\n\n---\n\n' : '\n\n'}${localize('shellIntegration.enabled', "Shell integration activated")}`; - } else { - if (instance.shellLaunchConfig.ignoreShellIntegration) { - shellIntegrationString += `${markdown ? '\n\n---\n\n' : '\n\n'}${localize('launchFailed.exitCodeOnlyShellIntegration', "The terminal process failed to launch. Disabling shell integration with terminal.integrated.shellIntegration.enabled might help.")}`; - } else { - if (instance.usedShellIntegrationInjection) { - shellIntegrationString += `${markdown ? '\n\n---\n\n' : '\n\n'}${localize('shellIntegration.activationFailed', "Shell integration failed to activate")}`; - } - } - } - return shellIntegrationString; -} - -export function getShellProcessTooltip(instance: ITerminalInstance, markdown: boolean): string { +export function getShellProcessTooltip(instance: ITerminalInstance, showDetailed: boolean): string { const lines: string[] = []; if (instance.processId && instance.processId > 0) { @@ -59,8 +57,16 @@ export function getShellProcessTooltip(instance: ITerminalInstance, markdown: bo } if (instance.shellLaunchConfig.executable) { - let commandLine = instance.shellLaunchConfig.executable; - const args = asArray(instance.injectedArgs || instance.shellLaunchConfig.args || []).map(x => `'${x}'`).join(' '); + let commandLine = ''; + if (!showDetailed && instance.shellLaunchConfig.executable.length > 32) { + const base = basename(instance.shellLaunchConfig.executable); + const sepIndex = instance.shellLaunchConfig.executable.length - base.length - 1; + const sep = instance.shellLaunchConfig.executable.substring(sepIndex, sepIndex + 1); + commandLine += `…${sep}${base}`; + } else { + commandLine += instance.shellLaunchConfig.executable; + } + const args = asArray(instance.injectedArgs || instance.shellLaunchConfig.args || []).map(x => x.match(/\s/) ? `'${x}'` : x).join(' '); if (args) { commandLine += ` ${args}`; } @@ -68,5 +74,32 @@ export function getShellProcessTooltip(instance: ITerminalInstance, markdown: bo lines.push(localize('shellProcessTooltip.commandLine', 'Command line: {0}', commandLine)); } - return lines.length ? `${markdown ? '\n\n---\n\n' : '\n\n'}${lines.join('\n')}` : ''; + return lines.length ? `\n\n---\n\n${lines.join('\n')}` : ''; +} + +export function refreshShellIntegrationInfoStatus(instance: ITerminalInstance) { + if (!instance.xterm) { + return; + } + const cmdDetectionType = ( + instance.capabilities.get(TerminalCapability.CommandDetection)?.hasRichCommandDetection + ? localize('shellIntegration.rich', 'Rich') + : instance.capabilities.has(TerminalCapability.CommandDetection) + ? localize('shellIntegration.basic', 'Basic') + : instance.usedShellIntegrationInjection + ? localize('shellIntegration.injectionFailed', "Injection failed to activate") + : localize('shellIntegration.no', 'No') + ); + const seenSequences = Array.from(instance.xterm.shellIntegration.seenSequences); + const seenSequencesString = ( + seenSequences.length > 0 + ? ` (${seenSequences.map(e => `\`${e}\``).join(', ')})` + : '' + ); + instance.statusList.add({ + id: TerminalStatus.ShellIntegrationInfo, + severity: Severity.Info, + tooltip: `${localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}`, + detailedTooltip: `${localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}${seenSequencesString}` + }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 7e22be4d682..42ff0ac1879 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -50,6 +50,7 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { InstanceContext, TerminalContextActionRunner } from './terminalContextMenu.js'; import { MicrotaskDelay } from '../../../../base/common/symbols.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; export class TerminalViewPane extends ViewPane { private _parentDomElement: HTMLElement | undefined; @@ -660,7 +661,8 @@ class SingleTabHoverDelegate implements IHoverDelegate { constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @IHoverService private readonly _hoverService: IHoverService, - @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService + @IStorageService private readonly _storageService: IStorageService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, ) { } @@ -675,8 +677,8 @@ class SingleTabHoverDelegate implements IHoverDelegate { if (!instance) { return; } - const hoverInfo = getInstanceHoverInfo(instance); - return this._hoverService.showHover({ + const hoverInfo = getInstanceHoverInfo(instance, this._storageService); + return this._hoverService.showInstantHover({ ...options, content: hoverInfo.content, actions: hoverInfo.actions diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts index 235a4586fb8..5f6aec126a0 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts @@ -44,7 +44,7 @@ export class TerminalHover extends Disposable implements ITerminalWidget { return; } const target = new CellHoverTarget(container, this._targetOptions); - const hover = this._hoverService.showHover({ + const hover = this._hoverService.showInstantHover({ target, content: this._text, actions: this._actions, diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index b5f2e1f5325..86015654e38 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -198,6 +198,9 @@ if [ "$__vsc_stable" = "0" ]; then builtin printf "\e]633;P;ContinuationPrompt=$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')\a" fi +# Report this shell supports rich command detection +builtin printf '\e]633;P;HasRichCommandDetection=True\a' + __vsc_report_prompt() { # Expand the original PS1 similarly to how bash would normally # See https://stackoverflow.com/a/37137981 for technique diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 43738d95a18..50f859af05c 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -112,6 +112,9 @@ unset VSCODE_NONCE __vscode_shell_env_reporting="$VSCODE_SHELL_ENV_REPORTING" unset VSCODE_SHELL_ENV_REPORTING +# Report this shell supports rich command detection +builtin printf '\e]633;P;HasRichCommandDetection=True\a' + __vsc_prompt_start() { builtin printf '\e]633;A\a' } diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish index 3d6cf19c36e..f1ef2bd6c6e 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish @@ -21,6 +21,8 @@ and ! set --query VSCODE_SHELL_INTEGRATION or exit set --global VSCODE_SHELL_INTEGRATION 1 +set --global __vscode_shell_env_reporting $VSCODE_SHELL_ENV_REPORTING +set -e VSCODE_SHELL_ENV_REPORTING # Apply any explicit path prefix (see #99878) # On fish, '$fish_user_paths' is always prepended to the PATH, for both login and non-login shells, so we need @@ -149,14 +151,16 @@ function __vsc_update_cwd --on-event fish_prompt end end -function __vsc_update_env --on-event fish_prompt - __vsc_esc EnvSingleStart 1 - for line in (env) - set myVar (echo $line | awk -F= '{print $1}') - set myVal (echo $line | awk -F= '{print $2}') - __vsc_esc EnvSingleEntry $myVar (__vsc_escape_value "$myVal") +if test "$__vscode_shell_env_reporting" = "1" + function __vsc_update_env --on-event fish_prompt + __vsc_esc EnvSingleStart 1 + for line in (env) + set myVar (echo $line | awk -F= '{print $1}') + set myVal (echo $line | awk -F= '{print $2}') + __vsc_esc EnvSingleEntry $myVar (__vsc_escape_value "$myVal") + end + __vsc_esc EnvSingleEnd end - __vsc_esc EnvSingleEnd end # Sent at the start of the prompt. @@ -203,4 +207,8 @@ function __init_vscode_shell_integration end end end + +# Report this shell supports rich command detection +__vsc_esc P HasRichCommandDetection=True + __preserve_fish_prompt diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index de5e863b7db..f3d92e9c3d9 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -93,7 +93,7 @@ function Global:Prompt() { $Result += if ($pwd.Provider.Name -eq 'FileSystem') { "$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a" } # Send current environment variables as JSON - # OSC 633 ; Env ; ; + # OSC 633 ; EnvJson ; ; if ($__vscode_shell_env_reporting -eq "1") { $envMap = @{} Get-ChildItem Env: | ForEach-Object { $envMap[$_.Name] = $_.Value } @@ -124,12 +124,13 @@ function Global:Prompt() { # Only send the command executed sequence when PSReadLine is loaded, if not shell integration should # still work thanks to the command line sequence if (Get-Module -Name PSReadLine) { + [Console]::Write("$([char]0x1b)]633;P;HasRichCommandDetection=True`a") $__VSCodeOriginalPSConsoleHostReadLine = $function:PSConsoleHostReadLine function Global:PSConsoleHostReadLine { $CommandLine = $__VSCodeOriginalPSConsoleHostReadLine.Invoke() # Command line - # OSC 633 ; E ; ; ST + # OSC 633 ; E [; [; ]] ST $Result = "$([char]0x1b)]633;E;" $Result += $(__VSCode-Escape-Value $CommandLine) # Only send the nonce if the OS is not Windows 10 as it seems to echo to the terminal @@ -148,6 +149,12 @@ if (Get-Module -Name PSReadLine) { $CommandLine } + + # Set ContinuationPrompt property + $ContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt + if ($ContinuationPrompt) { + [Console]::Write("$([char]0x1b)]633;P;ContinuationPrompt=$(__VSCode-Escape-Value $ContinuationPrompt)`a") + } } # Set IsWindows property @@ -159,14 +166,6 @@ else { [Console]::Write("$([char]0x1b)]633;P;IsWindows=$IsWindows`a") } -# Set ContinuationPrompt property -if ($isStable -eq "0") { - $ContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt - if ($ContinuationPrompt) { - [Console]::Write("$([char]0x1b)]633;P;ContinuationPrompt=$(__VSCode-Escape-Value $ContinuationPrompt)`a") - } -} - # Set always on key handlers which map to default VS Code keybindings function Set-MappedKeyHandler { param ([string[]] $Chord, [string[]]$Sequence) diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 2a717f4a7ed..3683a60a46b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -370,6 +370,10 @@ export interface ITerminalStatus { * What to show for this status in the terminal's hover. */ tooltip?: string | undefined; + /** + * What to show for this status in the terminal's hover when details are toggled. + */ + detailedTooltip?: string | undefined; /** * Actions to expose on hover. */ diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 41575c9fc97..28aa98cc24c 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../base/common/codicons.js'; +import { IJSONSchemaSnippet } from '../../../../base/common/jsonSchema.js'; import { isMacintosh, isWindows } from '../../../../base/common/platform.js'; -import { IProductConfiguration } from '../../../../base/common/product.js'; import { localize } from '../../../../nls.js'; import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import product from '../../../../platform/product/common/product.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { TerminalLocationString, TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; import { terminalColorSchema, terminalIconSchema } from '../../../../platform/terminal/common/terminalPlatformConfiguration.js'; @@ -16,19 +17,19 @@ import { terminalContribConfiguration, TerminalContribSettingId } from '../termi import { DEFAULT_COMMANDS_TO_SKIP_SHELL, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, MAXIMUM_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, SUGGESTIONS_FONT_WEIGHT } from './terminal.js'; const terminalDescriptors = '\n- ' + [ - '`\${cwd}`: ' + localize("cwd", "the terminal's current working directory"), + '`\${cwd}`: ' + localize("cwd", "the terminal's current working directory."), '`\${cwdFolder}`: ' + localize('cwdFolder', "the terminal's current working directory, displayed for multi-root workspaces or in a single root workspace when the value differs from the initial working directory. On Windows, this will only be displayed when shell integration is enabled."), - '`\${workspaceFolder}`: ' + localize('workspaceFolder', "the workspace in which the terminal was launched"), - '`\${workspaceFolderName}`: ' + localize('workspaceFolderName', "the `name` of the workspace in which the terminal was launched"), - '`\${local}`: ' + localize('local', "indicates a local terminal in a remote workspace"), - '`\${process}`: ' + localize('process', "the name of the terminal process"), - '`\${progress}`: ' + localize('progress', "the progress state as reported by the `OSC 9;4` sequence"), - '`\${separator}`: ' + localize('separator', "a conditional separator {0} that only shows when surrounded by variables with values or static text.", '(` - `)'), - '`\${sequence}`: ' + localize('sequence', "the name provided to the terminal by the process"), - '`\${task}`: ' + localize('task', "indicates this terminal is associated with a task"), - '`\${shellType}`: ' + localize('shellType', "the detected shell type"), - '`\${shellCommand}`: ' + localize('shellCommand', "the command being executed according to shell integration. This also requires high confidence in the detected command line which may not work in some prompt frameworks."), - '`\${shellPromptInput}`: ' + localize('shellPromptInput', "the shell's full prompt input according to shell integration"), + '`\${workspaceFolder}`: ' + localize('workspaceFolder', "the workspace in which the terminal was launched."), + '`\${workspaceFolderName}`: ' + localize('workspaceFolderName', "the `name` of the workspace in which the terminal was launched."), + '`\${local}`: ' + localize('local', "indicates a local terminal in a remote workspace."), + '`\${process}`: ' + localize('process', "the name of the terminal process."), + '`\${progress}`: ' + localize('progress', "the progress state as reported by the `OSC 9;4` sequence."), + '`\${separator}`: ' + localize('separator', "a conditional separator {0} that only shows when it's surrounded by variables with values or static text.", '(` - `)'), + '`\${sequence}`: ' + localize('sequence', "the name provided to the terminal by the process."), + '`\${task}`: ' + localize('task', "indicates this terminal is associated with a task."), + '`\${shellType}`: ' + localize('shellType', "the detected shell type."), + '`\${shellCommand}`: ' + localize('shellCommand', "the command being executed according to shell integration. This also requires high confidence in the detected command line, which may not work in some prompt frameworks."), + '`\${shellPromptInput}`: ' + localize('shellPromptInput', "the shell's full prompt input according to shell integration."), ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations let terminalTitle = localize('terminalTitle', "Controls the terminal title. Variables are substituted based on the context:"); @@ -174,7 +175,7 @@ const terminalConfiguration: IConfigurationNode = { }, [TerminalSettingId.FontFamily]: { markdownDescription: localize('terminal.integrated.fontFamily', "Controls the font family of the terminal. Defaults to {0}'s value.", '`#editor.fontFamily#`'), - type: 'string' + type: 'string', }, [TerminalSettingId.FontLigaturesEnabled]: { markdownDescription: localize('terminal.integrated.fontLigatures.enabled', "Controls whether font ligatures are enabled in the terminal. Ligatures will only work if the configured {0} supports them.", `\`#${TerminalSettingId.FontFamily}#\``), @@ -486,10 +487,7 @@ const terminalConfiguration: IConfigurationNode = { markdownDescription: localize('terminal.integrated.windowsUseConptyDll', "Whether to use the experimental conpty.dll (v1.22.250204002) shipped with VS Code, instead of the one bundled with Windows."), type: 'boolean', tags: ['preview'], - default: ((): boolean => { - const productService = Registry.as(Extensions.Configuration).getConfigurations()[0] as IProductConfiguration; - return productService?.quality !== 'stable'; - })() + default: product.quality !== 'stable' }, [TerminalSettingId.SplitCwd]: { description: localize('terminal.integrated.splitCwd', "Controls the working directory a split terminal starts with."), @@ -613,10 +611,7 @@ const terminalConfiguration: IConfigurationNode = { [TerminalSettingId.ShellIntegrationEnvironmentReporting]: { markdownDescription: localize('terminal.integrated.shellIntegration.environmentReporting', "Controls whether to report the shell environment, enabling its use in features such as {0}. This may cause a slowdown when printing your shell's prompt.", `\`#${TerminalContribSettingId.SuggestEnabled}#\``), type: 'boolean', - default: ((): boolean => { - const productService = Registry.as(Extensions.Configuration).getConfigurations()[0] as IProductConfiguration; - return productService?.quality !== 'stable'; - })() + default: product.quality !== 'stable' }, [TerminalSettingId.SmoothScrolling]: { markdownDescription: localize('terminal.integrated.smoothScrolling', "Controls whether the terminal will scroll using an animation."), @@ -649,9 +644,13 @@ const terminalConfiguration: IConfigurationNode = { } }; -export function registerTerminalConfiguration() { +export async function registerTerminalConfiguration(getFontSnippets: () => Promise) { const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration(terminalConfiguration); + const fontsSnippets = await getFontSnippets(); + if (terminalConfiguration.properties) { + terminalConfiguration.properties[TerminalSettingId.FontFamily].defaultSnippets = fontsSnippets; + } } Registry.as(WorkbenchExtensions.ConfigurationMigration) diff --git a/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts b/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts index 6a495eca81b..915da5be420 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts @@ -7,6 +7,7 @@ export const enum TerminalStorageKeys { SuggestedRendererType = 'terminal.integrated.suggestedRendererType', TabsListWidthHorizontal = 'tabs-list-width-horizontal', TabsListWidthVertical = 'tabs-list-width-vertical', + TabsShowDetailed = 'terminal.integrated.tabs.showDetailed', DeprecatedEnvironmentVariableCollections = 'terminal.integrated.environmentVariableCollections', EnvironmentVariableCollections = 'terminal.integrated.environmentVariableCollectionsV2', TerminalBufferState = 'terminal.integrated.bufferState', diff --git a/src/vs/workbench/contrib/terminal/common/terminalStrings.ts b/src/vs/workbench/contrib/terminal/common/terminalStrings.ts index 101fb7f7e99..ed00de1f363 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalStrings.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalStrings.ts @@ -36,9 +36,9 @@ export const terminalStrings = { rename: localize2('workbench.action.terminal.rename', "Rename..."), toggleSizeToContentWidth: localize2('workbench.action.terminal.sizeToContentWidthInstance', "Toggle Size to Content Width"), focusHover: localize2('workbench.action.terminal.focusHover', "Focus Hover"), - sendSequence: localize2('workbench.action.terminal.sendSequence', "Send Custom Sequence To Terminal"), + sendSequence: localize2('workbench.action.terminal.sendSequence', "Send Custom Sequence to Terminal"), newWithCwd: localize2('workbench.action.terminal.newWithCwd', "Create New Terminal Starting in a Custom Working Directory"), renameWithArgs: localize2('workbench.action.terminal.renameWithArg', "Rename the Currently Active Terminal"), - scrollToPreviousCommand: localize2('workbench.action.terminal.scrollToPreviousCommand', "Scroll To Previous Command"), - scrollToNextCommand: localize2('workbench.action.terminal.scrollToNextCommand', "Scroll To Next Command") + scrollToPreviousCommand: localize2('workbench.action.terminal.scrollToPreviousCommand', "Scroll to Previous Command"), + scrollToNextCommand: localize2('workbench.action.terminal.scrollToNextCommand', "Scroll to Next Command") }; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 782aca3edb1..a18ee7ae7dd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -10,6 +10,7 @@ import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contex import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IChatWidgetService } from '../../../chat/browser/chat.js'; import { ChatAgentLocation } from '../../../chat/common/chatAgents.js'; +import { ChatContextKeys } from '../../../chat/common/chatContextKeys.js'; import { IChatService } from '../../../chat/common/chatService.js'; import { AbstractInline1ChatAction } from '../../../inlineChat/browser/inlineChatActions.js'; import { isDetachedTerminalInstance } from '../../../terminal/browser/terminal.js'; @@ -30,6 +31,7 @@ registerActiveXtermAction({ }, f1: true, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.hasChatAgent ), @@ -74,7 +76,10 @@ registerActiveXtermAction({ }], icon: Codicon.close, f1: true, - precondition: TerminalChatContextKeys.visible, + precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, + TerminalChatContextKeys.visible, + ), run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { return; @@ -90,6 +95,7 @@ registerActiveXtermAction({ shortTitle: localize2('run', 'Run'), category: AbstractInline1ChatAction.category, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), TerminalChatContextKeys.responseContainsCodeBlock, @@ -122,6 +128,7 @@ registerActiveXtermAction({ shortTitle: localize2('runFirst', 'Run First'), category: AbstractInline1ChatAction.category, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), TerminalChatContextKeys.responseContainsMultipleCodeBlocks @@ -154,6 +161,7 @@ registerActiveXtermAction({ category: AbstractInline1ChatAction.category, icon: Codicon.insert, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), TerminalChatContextKeys.responseContainsCodeBlock, @@ -186,6 +194,7 @@ registerActiveXtermAction({ shortTitle: localize2('insertFirst', 'Insert First'), category: AbstractInline1ChatAction.category, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), TerminalChatContextKeys.responseContainsMultipleCodeBlocks @@ -218,6 +227,7 @@ registerActiveXtermAction({ icon: Codicon.refresh, category: AbstractInline1ChatAction.category, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), ), @@ -259,6 +269,7 @@ registerActiveXtermAction({ title: localize2('viewInChat', 'View in Chat'), category: AbstractInline1ChatAction.category, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), ), diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index b2be6193f47..3a8cde6a962 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -5,29 +5,30 @@ import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { Dimension, getActiveWindow, IFocusTracker, trackFocus } from '../../../../../base/browser/dom.js'; +import { CancelablePromise, createCancelablePromise, DeferredPromise } from '../../../../../base/common/async.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, observableValue, type IObservable } from '../../../../../base/common/observable.js'; import { MicrotaskDelay } from '../../../../../base/common/symbols.js'; -import './media/terminalChatWidget.css'; import { localize } from '../../../../../nls.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ChatAgentLocation } from '../../../chat/common/chatAgents.js'; -import { InlineChatWidget } from '../../../inlineChat/browser/inlineChatWidget.js'; -import { ITerminalInstance, type IXtermTerminal } from '../../../terminal/browser/terminal.js'; -import { MENU_TERMINAL_CHAT_WIDGET_INPUT_SIDE_TOOLBAR, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from './terminalChat.js'; -import { TerminalStickyScrollContribution } from '../../stickyScroll/browser/terminalStickyScrollContribution.js'; -import { MENU_INLINE_CHAT_WIDGET_SECONDARY } from '../../../inlineChat/common/inlineChat.js'; -import { CancelablePromise, createCancelablePromise, DeferredPromise } from '../../../../../base/common/async.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { IChatAcceptInputOptions, showChatView } from '../../../chat/browser/chat.js'; -import { ChatModel, IChatResponseModel, isCellTextEditOperation } from '../../../chat/common/chatModel.js'; -import { IChatService, IChatProgress } from '../../../chat/common/chatService.js'; -import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { MenuId } from '../../../../../platform/actions/common/actions.js'; import type { IChatViewState } from '../../../chat/browser/chatWidget.js'; -import { autorun, observableValue, type IObservable } from '../../../../../base/common/observable.js'; +import { IChatAgentService } from '../../../chat/common/chatAgents.js'; +import { ChatModel, IChatResponseModel, isCellTextEditOperation } from '../../../chat/common/chatModel.js'; +import { IChatProgress, IChatService } from '../../../chat/common/chatService.js'; +import { ChatAgentLocation } from '../../../chat/common/constants.js'; +import { InlineChatWidget } from '../../../inlineChat/browser/inlineChatWidget.js'; +import { MENU_INLINE_CHAT_WIDGET_SECONDARY } from '../../../inlineChat/common/inlineChat.js'; +import { ITerminalInstance, type IXtermTerminal } from '../../../terminal/browser/terminal.js'; +import { TerminalStickyScrollContribution } from '../../stickyScroll/browser/terminalStickyScrollContribution.js'; +import './media/terminalChatWidget.css'; +import { MENU_TERMINAL_CHAT_WIDGET_INPUT_SIDE_TOOLBAR, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from './terminalChat.js'; const enum Constants { HorizontalMargin = 10, @@ -100,6 +101,7 @@ export class TerminalChatWidget extends Disposable { @IStorageService private readonly _storageService: IStorageService, @IViewsService private readonly _viewsService: IViewsService, @IInstantiationService instantiationService: IInstantiationService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, ) { super(); @@ -225,7 +227,8 @@ export class TerminalChatWidget extends Disposable { } private _resetPlaceholder() { - this.inlineChatWidget.placeholder = this._model.value?.welcomeMessage?.title ?? localize('askAI', 'Ask AI'); + const defaultAgent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Terminal); + this.inlineChatWidget.placeholder = defaultAgent?.description ?? localize('askAI', 'Ask AI'); } async reveal(viewState?: IChatViewState): Promise { diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/media/terminalSymbolIcons.css b/src/vs/workbench/contrib/terminalContrib/suggest/browser/media/terminalSymbolIcons.css new file mode 100644 index 00000000000..6d918851bad --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/media/terminalSymbolIcons.css @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .codicon.codicon-symbol-method-arrow, +.monaco-workbench .codicon.codicon-symbol-method-arrow { color: var(--vscode-terminalSymbolIcon-aliasForeground); } +.monaco-editor .codicon.codicon-flag, +.monaco-workbench .codicon.codicon-flag { color: var(--vscode-terminalSymbolIcon-flagForeground); } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index c769b337fed..fce7396cf0a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -17,7 +17,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { GeneralShellType, TerminalLocation } from '../../../../../platform/terminal/common/terminal.js'; import { ITerminalContribution, ITerminalInstance, IXtermTerminal } from '../../../terminal/browser/terminal.js'; -import { registerActiveInstanceAction } from '../../../terminal/browser/terminalActions.js'; +import { registerActiveInstanceAction, registerTerminalAction } from '../../../terminal/browser/terminalActions.js'; import { registerTerminalContribution, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; import { TerminalContextKeys } from '../../../terminal/common/terminalContextKey.js'; import { TerminalSuggestCommandId } from '../common/terminal.suggest.js'; @@ -32,6 +32,7 @@ import { SuggestDetailsClassName } from '../../../../services/suggest/browser/si import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; +import './terminalSymbolIcons.js'; registerSingleton(ITerminalCompletionService, TerminalCompletionService, InstantiationType.Delayed); @@ -201,6 +202,23 @@ registerTerminalContribution(TerminalSuggestContribution.ID, TerminalSuggestCont // #region Actions +registerTerminalAction({ + id: TerminalSuggestCommandId.ConfigureSettings, + title: localize2('workbench.action.terminal.configureSuggestSettings', 'Configure'), + f1: false, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Comma, + weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.MenubarTerminalSuggestStatusMenu, + group: 'right', + order: 1 + }, + run: (c, accessor) => accessor.get(IPreferencesService).openSettings({ query: terminalSuggestConfigSection }) +}); + registerActiveInstanceAction({ id: TerminalSuggestCommandId.RequestCompletions, title: localize2('workbench.action.terminal.requestCompletions', 'Request Completions'), @@ -227,7 +245,8 @@ registerActiveInstanceAction({ keybinding: { // Up is bound to other workbench keybindings that this needs to beat primary: KeyCode.UpArrow, - weight: KeybindingWeight.WorkbenchContrib + 1 + weight: KeybindingWeight.WorkbenchContrib + 1, + when: ContextKeyExpr.or(SimpleSuggestContext.HasNavigated, ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.UpArrowNavigatesHistory}`, false)) }, run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.selectPreviousSuggestion() }); @@ -359,20 +378,20 @@ registerActiveInstanceAction({ }); registerActiveInstanceAction({ - id: TerminalSuggestCommandId.ConfigureSettings, - title: localize2('workbench.action.terminal.configureSuggestSettings', 'Configure'), + id: TerminalSuggestCommandId.HideSuggestWidgetAndNavigateHistory, + title: localize2('workbench.action.terminal.hideSuggestWidgetAndNavigateHistory', 'Hide Suggest Widget and Navigate History'), f1: false, precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible), - keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Comma, - weight: KeybindingWeight.WorkbenchContrib + keybinding: + { + primary: KeyCode.UpArrow, + when: ContextKeyExpr.and(SimpleSuggestContext.HasNavigated.negate(), ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.UpArrowNavigatesHistory}`, true)), + weight: KeybindingWeight.WorkbenchContrib + 2 }, - menu: { - id: MenuId.MenubarTerminalSuggestStatusMenu, - group: 'right', - order: 1 - }, - run: (activeInstance, c, accessor) => accessor.get(IPreferencesService).openSettings({ query: terminalSuggestConfigSection }) + run: (activeInstance) => { + TerminalSuggestContribution.get(activeInstance)?.addon?.hideSuggestWidget(true); + activeInstance.sendText('\u001b[A', false); // Up arrow + } }); registerActiveInstanceAction({ diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts index c83bbe13a7c..4cb466f933c 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts @@ -47,7 +47,11 @@ const compareCompletionsFn = (leadingLineContent: string, a: TerminalCompletionI // Sort files of the same name by extension const isArg = leadingLineContent.includes(' '); - if (!isArg && a.labelLowExcludeFileExt === b.labelLowExcludeFileExt) { + if (!isArg && a.completion.kind === TerminalCompletionItemKind.File && b.completion.kind === TerminalCompletionItemKind.File) { + // If the file name excluding the extension is different, just do a regular sort + if (a.labelLowExcludeFileExt !== b.labelLowExcludeFileExt) { + return a.labelLowExcludeFileExt.localeCompare(b.labelLowExcludeFileExt, undefined, { ignorePunctuation: true }); + } // Then by label length ascending (excluding file extension if it's a file) score = a.labelLowExcludeFileExt.length - b.labelLowExcludeFileExt.length; if (score !== 0) { @@ -81,20 +85,22 @@ const compareCompletionsFn = (leadingLineContent: string, a: TerminalCompletionI } // Sort by folder depth (eg. `vscode/` should come before `vscode-.../`) - if (a.labelLowNormalizedPath && b.labelLowNormalizedPath) { - // Directories - // Count depth of path (number of / or \ occurrences) - score = count(a.labelLowNormalizedPath, '/') - count(b.labelLowNormalizedPath, '/'); - if (score !== 0) { - return score; - } + if (a.completion.kind === TerminalCompletionItemKind.Folder && b.completion.kind === TerminalCompletionItemKind.Folder) { + if (a.labelLowNormalizedPath && b.labelLowNormalizedPath) { + // Directories + // Count depth of path (number of / or \ occurrences) + score = count(a.labelLowNormalizedPath, '/') - count(b.labelLowNormalizedPath, '/'); + if (score !== 0) { + return score; + } - // Ensure shorter prefixes appear first - if (b.labelLowNormalizedPath.startsWith(a.labelLowNormalizedPath)) { - return -1; // `a` is a prefix of `b`, so `a` should come first - } - if (a.labelLowNormalizedPath.startsWith(b.labelLowNormalizedPath)) { - return 1; // `b` is a prefix of `a`, so `b` should come first + // Ensure shorter prefixes appear first + if (b.labelLowNormalizedPath.startsWith(a.labelLowNormalizedPath)) { + return -1; // `a` is a prefix of `b`, so `a` should come first + } + if (a.labelLowNormalizedPath.startsWith(b.labelLowNormalizedPath)) { + return 1; // `b` is a prefix of `a`, so `b` should come first + } } } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index eb7f998cc39..af55e2bf0ff 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -16,6 +16,7 @@ import { TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration import { TerminalCompletionItemKind, type ITerminalCompletion } from './terminalCompletionItem.js'; import { env as processEnv } from '../../../../../base/common/process.js'; import type { IProcessEnvironment } from '../../../../../base/common/platform.js'; +import { timeout } from '../../../../../base/common/async.js'; export const ITerminalCompletionService = createDecorator('terminalCompletionService'); @@ -166,7 +167,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo if (provider.shellTypes && !provider.shellTypes.includes(shellType)) { return undefined; } - const completions = await provider.provideCompletions(promptValue, cursorPosition, allowFallbackCompletions, token); + const completions = await Promise.race([ + provider.provideCompletions(promptValue, cursorPosition, allowFallbackCompletions, token), + timeout(5000) + ]); if (!completions) { return undefined; } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 693bfefc3bb..1fe3d048e72 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -36,6 +36,8 @@ import { GOLDEN_LINE_HEIGHT_RATIO, MINIMUM_LINE_HEIGHT } from '../../../../../ed import { TerminalCompletionModel } from './terminalCompletionModel.js'; import { TerminalCompletionItem, TerminalCompletionItemKind, type ITerminalCompletion } from './terminalCompletionItem.js'; import { IntervalTimer, TimeoutTimer } from '../../../../../base/common/async.js'; +import { localize } from '../../../../../nls.js'; +import { TerminalSuggestTelemetry } from './terminalSuggestTelemetry.js'; export interface ISuggestController { isPasting: boolean; @@ -44,7 +46,7 @@ export interface ISuggestController { selectNextSuggestion(): void; selectNextPageSuggestion(): void; acceptSelectedSuggestion(suggestion?: Pick, 'item' | 'model'>): void; - hideSuggestWidget(cancelAnyRequests: boolean): void; + hideSuggestWidget(cancelAnyRequests: boolean, wasClosedByUser?: boolean): void; } export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggestController { private _terminal?: Terminal; @@ -101,6 +103,19 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest [TerminalCompletionItemKind.InlineSuggestionAlwaysOnTop, Codicon.star], ]); + private _kindToTypeMap = new Map([ + [TerminalCompletionItemKind.File, localize('file', 'File')], + [TerminalCompletionItemKind.Folder, localize('folder', 'Folder')], + [TerminalCompletionItemKind.Method, localize('method', 'Method')], + [TerminalCompletionItemKind.Alias, localize('alias', 'Alias')], + [TerminalCompletionItemKind.Argument, localize('argument', 'Argument')], + [TerminalCompletionItemKind.Option, localize('option', 'Option')], + [TerminalCompletionItemKind.OptionValue, localize('optionValue', 'Option Value')], + [TerminalCompletionItemKind.Flag, localize('flag', 'Flag')], + [TerminalCompletionItemKind.InlineSuggestion, localize('inlineSuggestion', 'Inline Suggestion')], + [TerminalCompletionItemKind.InlineSuggestionAlwaysOnTop, localize('inlineSuggestionAlwaysOnTop', 'Inline Suggestion')], + ]); + private readonly _inlineCompletion: ITerminalCompletion = { label: '', // Right arrow is used to accept the completion. This is a common keybinding in pwsh, zsh @@ -111,11 +126,13 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest provider: 'core', detail: 'Inline suggestion', kind: TerminalCompletionItemKind.InlineSuggestion, + kindLabel: 'Inline suggestion', icon: this._kindToIconMap.get(TerminalCompletionItemKind.InlineSuggestion), }; private readonly _inlineCompletionItem = new TerminalCompletionItem(this._inlineCompletion); private _shouldSyncWhenReady: boolean = false; + private _suggestTelemetry: TerminalSuggestTelemetry | undefined; constructor( shellType: TerminalShellType | undefined, @@ -160,10 +177,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (commandDetection) { if (this._promptInputModel !== commandDetection.promptInputModel) { this._promptInputModel = commandDetection.promptInputModel; + this._suggestTelemetry = this._register(this._instantiationService.createInstance(TerminalSuggestTelemetry, commandDetection, this._promptInputModel)); this._promptInputModelSubscriptions.value = combinedDisposable( this._promptInputModel.onDidChangeInput(e => this._sync(e)), this._promptInputModel.onDidFinishInput(() => { - this._mostRecentPromptInputState = undefined; this.hideSuggestWidget(true); }), ); @@ -257,6 +274,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest const completions = providedCompletions?.flat() || []; if (!explicitlyInvoked && !completions.length) { + this.hideSuggestWidget(true); return; } @@ -290,6 +308,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest for (const completion of completions) { if (!completion.icon && completion.kind !== undefined) { completion.icon = this._kindToIconMap.get(completion.kind); + completion.kindLabel = this._kindToTypeMap.get(completion.kind); } } @@ -353,7 +372,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (!this._wasLastInputVerticalArrowKey()) { // Only request on trigger character when it's a regular input, or on an arrow if the widget // is already visible - if (!this._wasLastInputArrowKey() || this._terminalSuggestWidgetVisibleContextKey.get()) { + if (!this._wasLastInputIncludedEscape() || this._terminalSuggestWidgetVisibleContextKey.get()) { this.requestCompletions(); return true; } @@ -369,6 +388,14 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return !!this._lastUserData?.match(/^\x1b[\[O]?[A-B]$/); } + /** + * Whether the last input included the escape character. Typically this will mean it was more + * than just a simple character, such as arrow keys, home, end, etc. + */ + private _wasLastInputIncludedEscape(): boolean { + return !!this._lastUserData?.includes('\x1b'); + } + private _wasLastInputArrowKey(): boolean { // Never request completions if the last key sequence was up or down as the user was likely // navigating history @@ -426,16 +453,20 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } // If the cursor moved to the left - if (this._mostRecentPromptInputState && promptInputState.cursorIndex < this._mostRecentPromptInputState.cursorIndex) { - // Backspace or left past a trigger character - if (config.suggestOnTriggerCharacters && !sent && this._mostRecentPromptInputState.cursorIndex > 0) { - const char = this._mostRecentPromptInputState.value[this._mostRecentPromptInputState.cursorIndex - 1]; - if ( - // Only trigger on `\` and `/` if it's a directory. Not doing so causes problems - // with git branches in particular - this._isFilteringDirectories && char.match(/[\\\/]$/) - ) { - sent = this._requestTriggerCharQuickSuggestCompletions(); + if (this._mostRecentPromptInputState && promptInputState.cursorIndex < this._mostRecentPromptInputState.cursorIndex && promptInputState.cursorIndex > 0) { + // We only want to refresh via trigger characters in this case if the widget is + // already visible + if (this._terminalSuggestWidgetVisibleContextKey.get()) { + // Backspace or left past a trigger character + if (config.suggestOnTriggerCharacters && !sent && this._mostRecentPromptInputState.cursorIndex > 0) { + const char = this._mostRecentPromptInputState.value[this._mostRecentPromptInputState.cursorIndex - 1]; + if ( + // Only trigger on `\` and `/` if it's a directory. Not doing so causes problems + // with git branches in particular + this._isFilteringDirectories && char.match(/[\\\/]$/) + ) { + sent = this._requestTriggerCharQuickSuggestCompletions(); + } } } } @@ -501,7 +532,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return; } const xtermBox = this._screen!.getBoundingClientRect(); - this._suggestWidget.showSuggestions(0, false, false, { + this._suggestWidget.showSuggestions(0, false, true, { left: xtermBox.left + this._terminal.buffer.active.cursorX * dimensions.width, top: xtermBox.top + this._terminal.buffer.active.cursorY * dimensions.height, height: dimensions.height @@ -592,6 +623,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return fontInfo; } + private _getAdvancedExplainModeDetails(): string | undefined { + return `promptInputModel: ${this._promptInputModel?.getCombinedString()}`; + } + private _showCompletions(model: TerminalCompletionModel, explicitlyInvoked?: boolean): void { if (!this._terminal?.element) { return; @@ -608,8 +643,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return; } const xtermBox = this._screen!.getBoundingClientRect(); - - suggestWidget.showSuggestions(0, false, false, { + suggestWidget.showSuggestions(0, false, !explicitlyInvoked, { left: xtermBox.left + this._terminal.buffer.active.cursorX * dimensions.width, top: xtermBox.top + this._terminal.buffer.active.cursorY * dimensions.height, height: dimensions.height @@ -628,7 +662,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest showStatusBarSettingId: TerminalSuggestSettingId.ShowStatusBar }, this._getFontInfo.bind(this), - this._onDidFontConfigurationChange.event.bind(this) + this._onDidFontConfigurationChange.event.bind(this), + this._getAdvancedExplainModeDetails.bind(this) )) as any as SimpleSuggestWidget; this._suggestWidget.list.style(getListStyles({ listInactiveFocusBackground: editorSuggestWidgetSelectedBackground, @@ -690,6 +725,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } const initialPromptInputState = this._mostRecentPromptInputState; if (!suggestion || !initialPromptInputState || this._leadingLineContent === undefined || !this._model) { + this._suggestTelemetry?.acceptCompletion(undefined, this._mostRecentPromptInputState?.value); return; } SuggestAddon.lastAcceptedCompletionTimestamp = Date.now(); @@ -775,6 +811,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest // Send the completion this._onAcceptedCompletion.fire(resultSequence); + this._suggestTelemetry?.acceptCompletion(completion, this._mostRecentPromptInputState?.value); this.hideSuggestWidget(true); } @@ -826,3 +863,4 @@ export function normalizePathSeparator(path: string, sep: string): string { } return path.replaceAll('/', '\\'); } + diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestTelemetry.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestTelemetry.ts new file mode 100644 index 00000000000..be5d761ebcc --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestTelemetry.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { ICommandDetectionCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { IPromptInputModel } from '../../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js'; +import { ITerminalCompletion } from './terminalCompletionItem.js'; + +export class TerminalSuggestTelemetry extends Disposable { + private _acceptedCompletions: Array<{ label: string; kindLabel?: string }> | undefined; + + constructor( + commandDetection: ICommandDetectionCapability, + private readonly _promptInputModel: IPromptInputModel, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + ) { + super(); + this._register(commandDetection.onCommandFinished((e) => { + this._sendTelemetryInfo(false, e.exitCode); + this._acceptedCompletions = undefined; + })); + this._register(this._promptInputModel.onDidInterrupt(() => { + this._sendTelemetryInfo(true); + this._acceptedCompletions = undefined; + })); + } + acceptCompletion(completion: ITerminalCompletion | undefined, commandLine?: string): void { + if (!completion || !commandLine) { + this._acceptedCompletions = undefined; + return; + } + this._acceptedCompletions = this._acceptedCompletions || []; + this._acceptedCompletions.push({ label: typeof completion.label === 'string' ? completion.label : completion.label.label, kindLabel: completion.kindLabel }); + } + private _sendTelemetryInfo(fromInterrupt?: boolean, exitCode?: number): void { + const commandLine = this._promptInputModel?.value; + for (const completion of this._acceptedCompletions || []) { + const label = completion?.label; + const kind = completion?.kindLabel; + if (label === undefined || commandLine === undefined || kind === undefined) { + return; + } + + let outcome: CompletionOutcome; + if (fromInterrupt) { + outcome = CompletionOutcome.Interrupted; + } else if (commandLine.trim() && commandLine.includes(label)) { + outcome = CompletionOutcome.Accepted; + } else if (inputContainsFirstHalfOfLabel(commandLine, label)) { + outcome = CompletionOutcome.AcceptedWithEdit; + } else { + outcome = CompletionOutcome.Deleted; + } + this._telemetryService.publicLog2<{ + kind: string | undefined; + outcome: CompletionOutcome; + exitCode: number | undefined; + }, { + owner: 'meganrogge'; + comment: 'This data is collected to understand the outcome of a terminal completion acceptance.'; + kind: { + classification: 'SystemMetaData'; + purpose: 'FeatureInsight'; + comment: 'The completion item\'s kind'; + }; + outcome: { + classification: 'SystemMetaData'; + purpose: 'FeatureInsight'; + comment: 'The outcome of the accepted completion'; + }; + exitCode: { + classification: 'SystemMetaData'; + purpose: 'FeatureInsight'; + comment: 'The exit code from the command'; + }; + }>('terminal.suggest.acceptedCompletion', { + kind, + outcome, + exitCode + }); + } + } +} + +function inputContainsFirstHalfOfLabel(commandLine: string, label: string): boolean { + return commandLine.includes(label.substring(0, Math.ceil(label.length / 2))); +} + +const enum CompletionOutcome { + Accepted = 'Accepted', + Deleted = 'Deleted', + AcceptedWithEdit = 'AcceptedWithEdit', + Interrupted = 'Interrupted' +} diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSymbolIcons.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSymbolIcons.ts new file mode 100644 index 00000000000..a2039d2cd57 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSymbolIcons.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/terminalSymbolIcons.css'; +import { SYMBOL_ICON_ENUMERATOR_FOREGROUND, SYMBOL_ICON_METHOD_FOREGROUND } from '../../../../../editor/contrib/symbolIcons/browser/symbolIcons.js'; +import { registerColor } from '../../../../../platform/theme/common/colorUtils.js'; +import { localize } from '../../../../../nls.js'; + +export const TERMINAL_SYMBOL_ICON_FLAG_FOREGROUND = registerColor('terminalSymbolIcon.flagForeground', SYMBOL_ICON_ENUMERATOR_FOREGROUND, localize('terminalSymbolIcon.flagForeground', 'The foreground color for an flag icon. These icons will appear in the terminal suggest widget.')); + +export const TERMINAL_SYMBOL_ICON_ALIAS_FOREGROUND = registerColor('terminalSymbolIcon.aliasForeground', SYMBOL_ICON_METHOD_FOREGROUND, localize('terminalSymbolIcon.aliasForeground', 'The foreground color for an alias icon. These icons will appear in the terminal suggest widget.')); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts index 30d9c86ae46..f472ffd7673 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts @@ -11,6 +11,7 @@ export const enum TerminalSuggestCommandId { AcceptSelectedSuggestion = 'workbench.action.terminal.acceptSelectedSuggestion', AcceptSelectedSuggestionEnter = 'workbench.action.terminal.acceptSelectedSuggestionEnter', HideSuggestWidget = 'workbench.action.terminal.hideSuggestWidget', + HideSuggestWidgetAndNavigateHistory = 'workbench.action.terminal.hideSuggestWidgetAndNavigateHistory', ClearSuggestCache = 'workbench.action.terminal.clearSuggestCache', RequestCompletions = 'workbench.action.terminal.requestCompletions', ResetWidgetSize = 'workbench.action.terminal.resetSuggestWidgetSize', diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index db600bb842b..e4e4e73ca3f 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -18,6 +18,7 @@ export const enum TerminalSuggestSettingId { ShowStatusBar = 'terminal.integrated.suggest.showStatusBar', CdPath = 'terminal.integrated.suggest.cdPath', InlineSuggestion = 'terminal.integrated.suggest.inlineSuggestion', + UpArrowNavigatesHistory = 'terminal.integrated.suggest.upArrowNavigatesHistory', } export const windowsDefaultExecutableExtensions: string[] = [ @@ -169,7 +170,14 @@ export const terminalSuggestConfiguration: IStringDictionary { - const testActions: IAction[] = []; + const testActions: Action[] = []; const capabilities = this.testProfileService.capabilitiesForTest(test.item); [ @@ -1067,7 +1067,7 @@ abstract class RunTestDecoration { () => this.commandService.executeCommand('_revealTestInExplorer', test.item.extId))); const contributed = this.getContributedTestActions(test, capabilities); - return { object: Separator.join(testActions, contributed), dispose() { } }; + return { object: Separator.join(testActions, contributed), dispose() { testActions.forEach(a => a.dispose()); } }; } private getContributedTestActions(test: InternalTestItem, capabilities: number): IAction[] { @@ -1110,8 +1110,8 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio } public override getContextMenuActions() { - const allActions: IAction[] = []; - + const disposable = new DisposableStore(); + const allActions: Action[] = []; [ { bitset: TestRunProfileBitset.Run, label: localize('run all test', 'Run All Tests') }, { bitset: TestRunProfileBitset.Coverage, label: localize('run all test with coverage', 'Run All Tests with Coverage') }, @@ -1123,6 +1123,8 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio } }); + disposable.add(toDisposable(() => allActions.forEach(a => a.dispose()))); + const testItems = this.tests.map((testItem): IMultiRunTest => ({ currentLabel: testItem.test.item.label, testItem, @@ -1157,7 +1159,6 @@ class MultiRunTestDecoration extends RunTestDecoration implements ITestDecoratio return (ai.sortText || ai.label).localeCompare(bi.sortText || bi.label); }); - const disposable = new DisposableStore(); let testSubmenus: IAction[] = testItems.map(({ currentLabel, testItem }) => { const actions = this.getTestContextMenuActions(testItem.test, testItem.resultItem); disposable.add(actions); diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index e8e7b606c86..678d939d9c3 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -809,7 +809,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { order: 7 } satisfies ISubmenuItem); MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - title: localize({ key: 'miSelectTheme', comment: ['&& denotes a mnemonic'] }, "&&Theme"), + title: localize({ key: 'miSelectTheme', comment: ['&& denotes a mnemonic'] }, "&&Themes"), submenu: ThemesSubMenu, group: '2_configuration', order: 7 diff --git a/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts index a5806666f08..fd1cd59d469 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.test.contribution.ts @@ -286,7 +286,7 @@ class Snapper { const capture = tokenizationSupport?.captureAtPositionTree(cursor.currentNode.startPosition.row + 1, cursor.currentNode.startPosition.column + 1, tree); tokens.push({ c: cursor.currentNode.text.replace(/\r/g, ''), - t: capture && capture.length > 0 ? capture[capture.length - 1].name : '', + t: capture?.map(cap => cap.name).join(' ') ?? '', r: { dark_plus: undefined, light_plus: undefined, diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index fd3452f5f02..3def92a9336 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -29,7 +29,7 @@ import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys import { Promises } from '../../../../base/common/async.js'; import { IUserDataSyncWorkbenchService } from '../../../services/userDataSync/common/userDataSync.js'; import { Event } from '../../../../base/common/event.js'; -import { Action } from '../../../../base/common/actions.js'; +import { toAction } from '../../../../base/common/actions.js'; export const CONTEXT_UPDATE_STATE = new RawContextKey('updateState', StateType.Uninitialized); export const MAJOR_MINOR_UPDATE_AVAILABLE = new RawContextKey('majorMinorUpdateAvailable', false); @@ -215,8 +215,10 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu message: nls.localize('update service disabled', "Updates are disabled because you are running the user-scope installation of {0} as Administrator.", this.productService.nameLong), actions: { primary: [ - new Action('', nls.localize('learn more', "Learn More"), undefined, undefined, () => { - this.openerService.open('https://aka.ms/vscode-windows-setup'); + toAction({ + id: '', + label: nls.localize('learn more', "Learn More"), + run: () => this.openerService.open('https://aka.ms/vscode-windows-setup') }) ] }, diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index c4a03276d86..04a69674350 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -5,7 +5,7 @@ import './media/userDataProfilesEditor.css'; import { $, addDisposableListener, append, clearNode, Dimension, EventHelper, EventType, IDomPosition, trackFocus } from '../../../../base/browser/dom.js'; -import { Action, IAction, IActionChangeEvent, Separator, SubmenuAction } from '../../../../base/common/actions.js'; +import { Action, IAction, IActionChangeEvent, Separator, SubmenuAction, toAction } from '../../../../base/common/actions.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; @@ -225,7 +225,11 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi actions.push(new SubmenuAction('from.template', localize('from template', "From Template"), this.getCreateFromTemplateActions())); actions.push(new Separator()); } - actions.push(new Action('importProfile', localize('importProfile', "Import Profile..."), undefined, true, () => this.importProfile())); + actions.push(toAction({ + id: 'importProfile', + label: localize('importProfile', "Import Profile..."), + run: () => this.importProfile() + })); return actions; } }, @@ -240,12 +244,11 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi private getCreateFromTemplateActions(): IAction[] { return this.templates.map(template => - new Action( - `template:${template.url}`, - template.name, - undefined, - true, - () => this.createNewProfile(URI.parse(template.url)))); + toAction({ + id: `template:${template.url}`, + label: template.name, + run: () => this.createNewProfile(URI.parse(template.url)) + })); } private registerListeners(): void { @@ -282,13 +285,21 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi private getTreeContextMenuActions(): IAction[] { const actions: IAction[] = []; - actions.push(new Action('newProfile', localize('newProfile', "New Profile"), undefined, true, () => this.createNewProfile())); + actions.push(toAction({ + id: 'newProfile', + label: localize('newProfile', "New Profile"), + run: () => this.createNewProfile() + })); const templateActions = this.getCreateFromTemplateActions(); if (templateActions.length) { actions.push(new SubmenuAction('from.template', localize('new from template', "New Profile From Template"), templateActions)); } actions.push(new Separator()); - actions.push(new Action('importProfile', localize('importProfile', "Import Profile..."), undefined, true, () => this.importProfile())); + actions.push(toAction({ + id: 'importProfile', + label: localize('importProfile', "Import Profile..."), + run: () => this.importProfile() + })); return actions; } @@ -1036,7 +1047,7 @@ class ProfileIconRenderer extends ProfilePropertyRenderer { return; } iconSelectBox.clearInput(); - hoverWidget = this.hoverService.showHover({ + hoverWidget = this.hoverService.showInstantHover({ content: iconSelectBox.domNode, target: iconElement, position: { diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts index 6cd4321ee42..5ef8a898c17 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action, IAction, Separator } from '../../../../base/common/actions.js'; +import { Action, IAction, Separator, toAction } from '../../../../base/common/actions.js'; import { Emitter } from '../../../../base/common/event.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; @@ -262,11 +262,12 @@ export abstract class AbstractUserDataProfileElement extends Disposable { checkbox: undefined, resourceType: r, openAction: children.length - ? new Action('_open', - localize('open', "Open to the Side"), - ThemeIcon.asClassName(Codicon.goToFile), - true, - () => children[0]?.openAction?.run()) + ? toAction({ + id: '_open', + label: localize('open', "Open to the Side"), + class: ThemeIcon.asClassName(Codicon.goToFile), + run: () => children[0]?.openAction?.run() + }) : undefined }; })); @@ -309,11 +310,16 @@ export abstract class AbstractUserDataProfileElement extends Disposable { description: isString(child.description) ? child.description : undefined, resource: URI.revive(child.resourceUri), icon: child.themeIcon, - openAction: new Action('_openChild', localize('open', "Open to the Side"), ThemeIcon.asClassName(Codicon.goToFile), true, async () => { - if (child.parent.type === ProfileResourceType.Extensions) { - await this.commandService.executeCommand('extension.open', child.handle, undefined, true, undefined, true); - } else if (child.resourceUri) { - await this.commandService.executeCommand(API_OPEN_EDITOR_COMMAND_ID, child.resourceUri, [SIDE_GROUP], undefined); + openAction: toAction({ + id: '_openChild', + label: localize('open', "Open to the Side"), + class: ThemeIcon.asClassName(Codicon.goToFile), + run: async () => { + if (child.parent.type === ProfileResourceType.Extensions) { + await this.commandService.executeCommand('extension.open', child.handle, undefined, true, undefined, true); + } else if (child.resourceUri) { + await this.commandService.executeCommand(API_OPEN_EDITOR_COMMAND_ID, child.resourceUri, [SIDE_GROUP], undefined); + } } }), actions: { @@ -1036,13 +1042,13 @@ export class UserDataProfilesEditorModel extends EditorModel { )); primaryActions.push(createAction); if (isWeb && copyFrom instanceof URI && isProfileURL(copyFrom)) { - primaryActions.push(new Action( + primaryActions.push(disposables.add(new Action( 'userDataProfile.createInDesktop', localize('import in desktop', "Create in {0}", this.productService.nameLong), undefined, true, () => this.openerService.open(copyFrom, { openExternal: true }) - )); + ))); } const cancelAction = disposables.add(new Action( 'userDataProfile.cancel', diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts index 3da71ce186d..10d8716cd83 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts @@ -13,7 +13,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { localize } from '../../../../nls.js'; import { isWeb } from '../../../../base/common/platform.js'; import { UserDataSyncTrigger } from './userDataSyncTrigger.js'; -import { Action } from '../../../../base/common/actions.js'; +import { toAction } from '../../../../base/common/actions.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IHostService } from '../../../services/host/browser/host.js'; @@ -42,8 +42,16 @@ class UserDataSyncReportIssueContribution extends Disposable implements IWorkben message, actions: { primary: [ - new Action('Show Sync Logs', localize('show sync logs', "Show Log"), undefined, true, () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID)), - new Action('Restart', isWeb ? localize('reload', "Reload") : localize('restart', "Restart"), undefined, true, () => this.hostService.restart()) + toAction({ + id: 'Show Sync Logs', + label: localize('show sync logs', "Show Log"), + run: () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID) + }), + toAction({ + id: 'Restart', + label: isWeb ? localize('reload', "Reload") : localize('restart', "Restart"), + run: () => this.hostService.restart() + }) ] } }); @@ -58,7 +66,11 @@ class UserDataSyncReportIssueContribution extends Disposable implements IWorkben source: error.operationId ? localize('settings sync', "Settings Sync. Operation Id: {0}", error.operationId) : undefined, actions: { primary: [ - new Action('Show Sync Logs', localize('show sync logs', "Show Log"), undefined, true, () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID)), + toAction({ + id: 'Show Sync Logs', + label: localize('show sync logs', "Show Log"), + run: () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID) + }) ] } }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 8f2a4d379fa..5ab93b68536 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from '../../../../base/common/actions.js'; +import { toAction } from '../../../../base/common/actions.js'; import { getErrorMessage, isCancellationError } from '../../../../base/common/errors.js'; import { Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable, IDisposable } from '../../../../base/common/lifecycle.js'; @@ -249,7 +249,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo severity: Severity.Info, message: localize('session expired', "Settings sync was turned off because current session is expired, please sign in again to turn on sync."), actions: { - primary: [new Action('turn on sync', localize('turn on sync', "Turn on Settings Sync..."), undefined, true, () => this.turnOn())] + primary: [toAction({ + id: 'turn on sync', + label: localize('turn on sync', "Turn on Settings Sync..."), + run: () => this.turnOn() + })] } }); break; @@ -258,7 +262,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo severity: Severity.Info, message: localize('turned off', "Settings sync was turned off from another device, please turn on sync again."), actions: { - primary: [new Action('turn on sync', localize('turn on sync', "Turn on Settings Sync..."), undefined, true, () => this.turnOn())] + primary: [toAction({ + id: 'turn on sync', + label: localize('turn on sync', "Turn on Settings Sync..."), + run: () => this.turnOn() + })] } }); break; @@ -292,8 +300,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo message: operationId ? `${message} ${operationId}` : message, actions: { primary: [ - new Action('Show Sync Logs', localize('show sync logs', "Show Log"), undefined, true, () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID)), - new Action('Report Issue', localize('report issue', "Report Issue"), undefined, true, () => this.workbenchIssueService.openReporter()) + toAction({ + id: 'Show Sync Logs', + label: localize('show sync logs', "Show Log"), + run: () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID) + }), + toAction({ + id: 'Report Issue', + label: localize('report issue', "Report Issue"), + run: () => this.workbenchIssueService.openReporter() + }) ] } }); @@ -305,8 +321,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo message: localize('error reset required', "Settings sync is disabled because your data in the cloud is older than that of the client. Please clear your data in the cloud before turning on sync."), actions: { primary: [ - new Action('reset', localize('reset', "Clear Data in Cloud..."), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), - new Action('show synced data', localize('show synced data action', "Show Synced Data"), undefined, true, () => this.userDataSyncWorkbenchService.showSyncActivity()) + toAction({ + id: 'reset', + label: localize('reset', "Clear Data in Cloud..."), + run: () => this.userDataSyncWorkbenchService.resetSyncedData() + }), + toAction({ + id: 'show synced data', + label: localize('show synced data action', "Show Synced Data"), + run: () => this.userDataSyncWorkbenchService.showSyncActivity() + }) ] } }); @@ -337,7 +361,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo severity: Severity.Info, message: localize('service changed and turned off', "Settings sync was turned off because {0} now uses a separate service. Please turn on sync again.", this.productService.nameLong), actions: { - primary: [new Action('turn on sync', localize('turn on sync', "Turn on Settings Sync..."), undefined, true, () => this.turnOn())] + primary: [toAction({ + id: 'turn on sync', + label: localize('turn on sync', "Turn on Settings Sync..."), + run: () => this.turnOn() + })] } }); } @@ -351,8 +379,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo severity: Severity.Error, message: operationId ? `${message} ${operationId}` : message, actions: { - primary: [new Action('open sync file', localize('open file', "Open {0} File", getSyncAreaLabel(resource)), undefined, true, - () => resource === SyncResource.Settings ? this.preferencesService.openUserSettings({ jsonEditor: true }) : this.preferencesService.openGlobalKeybindingSettings(true))] + primary: [toAction({ + id: 'open sync file', + label: localize('open file', "Open {0} File", getSyncAreaLabel(resource)), + run: () => resource === SyncResource.Settings ? this.preferencesService.openUserSettings({ jsonEditor: true }) : this.preferencesService.openGlobalKeybindingSettings(true) + })] } }); } @@ -408,8 +439,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo severity: Severity.Error, message: localize('errorInvalidConfiguration', "Unable to sync {0} because the content in the file is not valid. Please open the file and correct it.", errorArea.toLowerCase()), actions: { - primary: [new Action('open sync file', localize('open file', "Open {0} File", errorArea), undefined, true, - () => source === SyncResource.Settings ? this.preferencesService.openUserSettings({ jsonEditor: true }) : this.preferencesService.openGlobalKeybindingSettings(true))] + primary: [toAction({ + id: 'open sync file', + label: localize('open file', "Open {0} File", errorArea), + run: () => source === SyncResource.Settings ? this.preferencesService.openUserSettings({ jsonEditor: true }) : this.preferencesService.openGlobalKeybindingSettings(true) + })] } }); this.invalidContentErrorDisposables.set(key, toDisposable(() => { @@ -494,8 +528,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo message: localize('error reset required while starting sync', "Settings sync cannot be turned on because your data in the cloud is older than that of the client. Please clear your data in the cloud before turning on sync."), actions: { primary: [ - new Action('reset', localize('reset', "Clear Data in Cloud..."), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), - new Action('show synced data', localize('show synced data action', "Show Synced Data"), undefined, true, () => this.userDataSyncWorkbenchService.showSyncActivity()) + toAction({ + id: 'reset', + label: localize('reset', "Clear Data in Cloud..."), + run: () => this.userDataSyncWorkbenchService.resetSyncedData() + }), + toAction({ + id: 'show synced data', + label: localize('show synced data action', "Show Synced Data"), + run: () => this.userDataSyncWorkbenchService.showSyncActivity() + }) ] } }); @@ -529,7 +571,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const items = this.getConfigureSyncQuickPickItems(); quickPick.items = items; - quickPick.selectedItems = items.filter(item => this.userDataSyncEnablementService.isResourceEnabled(item.id)); + quickPick.selectedItems = items.filter(item => this.userDataSyncEnablementService.isResourceEnabled(item.id, true)); let accepted: boolean = false; disposables.add(Event.any(quickPick.onDidAccept, quickPick.onDidCustom)(() => { accepted = true; @@ -575,8 +617,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo label: getSyncAreaLabel(SyncResource.Profiles), }]; - // if the `reusable prompt` feature is enabled, add appropriate item to the list - if (PromptsConfig.enabled(this.configService)) { + // if the `reusable prompt` feature is enabled and in vscode + // insiders, add the `Prompts` resource item to the list + if (PromptsConfig.enabled(this.configService) === true) { result.push({ id: SyncResource.Prompts, label: getSyncAreaLabel(SyncResource.Prompts) diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 8ced2a6fa5d..eb5f2a8eb14 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -20,7 +20,7 @@ import { IDialogService, IFileDialogService } from '../../../../platform/dialogs import { Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Action } from '../../../../base/common/actions.js'; +import { toAction } from '../../../../base/common/actions.js'; import { IUserDataSyncWorkbenchService, CONTEXT_SYNC_STATE, getSyncAreaLabel, CONTEXT_ACCOUNT_STATE, AccountStatus, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_TITLE, SYNC_CONFLICTS_VIEW_ID, CONTEXT_ENABLE_SYNC_CONFLICTS_VIEW, CONTEXT_HAS_CONFLICTS } from '../../../services/userDataSync/common/userDataSync.js'; import { IUserDataSyncMachinesService, IUserDataSyncMachine, isWebPlatform } from '../../../../platform/userDataSync/common/userDataSyncMachines.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; @@ -380,7 +380,11 @@ abstract class UserDataSyncActivityViewDataProvider implements ITre message: error.message, actions: { primary: [ - new Action('reset', localize('reset', "Reset Synced Data"), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), + toAction({ + id: 'reset', + label: localize('reset', "Reset Synced Data"), + run: () => this.userDataSyncWorkbenchService.resetSyncedData() + }), ] } }); diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 4985fa12fc7..1273b8c7a9a 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -5,7 +5,9 @@ import { isFirefox } from '../../../../base/browser/browser.js'; import { addDisposableListener, EventType, getWindowById } from '../../../../base/browser/dom.js'; +import { parentOriginHash } from '../../../../base/browser/iframe.js'; import { IMouseWheelEvent } from '../../../../base/browser/mouseEvent.js'; +import { CodeWindow } from '../../../../base/browser/window.js'; import { promiseWithResolvers, ThrottledDelayer } from '../../../../base/common/async.js'; import { streamToBuffer, VSBufferReadableStream } from '../../../../base/common/buffer.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; @@ -26,18 +28,15 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IRemoteAuthorityResolverService } from '../../../../platform/remote/common/remoteAuthorityResolver.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ITunnelService } from '../../../../platform/tunnel/common/tunnel.js'; import { WebviewPortMappingManager } from '../../../../platform/webview/common/webviewPortMapping.js'; -import { parentOriginHash } from '../../../../base/browser/iframe.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { decodeAuthority, webviewGenericCspSource, webviewRootResourceAuthority } from '../common/webview.js'; import { loadLocalResource, WebviewResourceResponse } from './resourceLoading.js'; import { WebviewThemeDataProvider } from './themeing.js'; import { areWebviewContentOptionsEqual, IWebview, WebviewContentOptions, WebviewExtensionDescription, WebviewInitInfo, WebviewMessageReceivedEvent, WebviewOptions } from './webview.js'; import { WebviewFindDelegate, WebviewFindWidget } from './webviewFindWidget.js'; import { FromWebviewMessage, KeyEvent, ToWebviewMessage, WebViewDragEvent } from './webviewMessages.js'; -import { decodeAuthority, webviewGenericCspSource, webviewRootResourceAuthority } from '../common/webview.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; -import { CodeWindow } from '../../../../base/browser/window.js'; interface WebviewContent { readonly html: string; @@ -158,7 +157,6 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD @IFileService private readonly _fileService: IFileService, @ILogService private readonly _logService: ILogService, @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, @ITunnelService private readonly _tunnelService: ITunnelService, @IInstantiationService instantiationService: IInstantiationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @@ -588,18 +586,6 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD if (this._environmentService.isExtensionDevelopment) { this._onMissingCsp.fire(this.extension.id); } - - const payload = { - extension: this.extension.id.value - } as const; - - type Classification = { - extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the extension that created the webview.' }; - owner: 'mjbvz'; - comment: 'Helps find which extensions are contributing webviews with invalid CSPs'; - }; - - this._telemetryService.publicLog2('webviewMissingCsp', payload); } } diff --git a/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts index f4fc6b8365f..13d75633fb4 100644 --- a/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts @@ -18,14 +18,13 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IRemoteAuthorityResolverService } from '../../../../platform/remote/common/remoteAuthorityResolver.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ITunnelService } from '../../../../platform/tunnel/common/tunnel.js'; import { FindInFrameOptions, IWebviewManagerService } from '../../../../platform/webview/common/webviewManagerService.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { WebviewThemeDataProvider } from '../browser/themeing.js'; import { WebviewInitInfo } from '../browser/webview.js'; import { WebviewElement } from '../browser/webviewElement.js'; import { WindowIgnoreMenuShortcutsManager } from './windowIgnoreMenuShortcutsManager.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; /** * Webview backed by an iframe but that uses Electron APIs to power the webview. @@ -48,7 +47,6 @@ export class ElectronWebviewElement extends WebviewElement { @IContextMenuService contextMenuService: IContextMenuService, @ITunnelService tunnelService: ITunnelService, @IFileService fileService: IFileService, - @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ILogService logService: ILogService, @@ -61,7 +59,7 @@ export class ElectronWebviewElement extends WebviewElement { ) { super(initInfo, webviewThemeDataProvider, configurationService, contextMenuService, notificationService, environmentService, - fileService, logService, remoteAuthorityResolverService, telemetryService, tunnelService, instantiationService, accessibilityService); + fileService, logService, remoteAuthorityResolverService, tunnelService, instantiationService, accessibilityService); this._webviewKeyboardHandler = new WindowIgnoreMenuShortcutsManager(configurationService, mainProcessService, _nativeHostService); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 39313994cd0..18322a787ac 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -719,8 +719,8 @@ export class GettingStartedPage extends EditorPane { const themeType = this.themeService.getColorTheme().type; const videoPath = media.path[themeType]; const videoPoster = media.poster ? media.poster[themeType] : undefined; - - const rawHTML = await this.detailsRenderer.renderVideo(videoPath, videoPoster); + const altText = media.altText ? media.altText : localize('videoAltText', "Video for {0}", stepToExpand.title); + const rawHTML = await this.detailsRenderer.renderVideo(videoPath, videoPoster, altText); this.webview.setHtml(rawHTML); let isDisposed = false; @@ -731,7 +731,7 @@ export class GettingStartedPage extends EditorPane { const themeType = this.themeService.getColorTheme().type; const videoPath = media.path[themeType]; const videoPoster = media.poster ? media.poster[themeType] : undefined; - const body = await this.detailsRenderer.renderVideo(videoPath, videoPoster); + const body = await this.detailsRenderer.renderVideo(videoPath, videoPoster, altText); if (!isDisposed) { // Make sure we weren't disposed of in the meantime this.webview.setHtml(body); @@ -1359,6 +1359,7 @@ export class GettingStartedPage extends EditorPane { container.appendChild(shortcutMessage); const label = new KeybindingLabel(shortcutMessage, OS, { ...defaultKeybindingLabelStyles }); label.set(keybinding); + this.detailsPageDisposables.add(label); } } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts index 9490776109b..1ca5db0b86b 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -201,7 +201,7 @@ export class GettingStartedDetailsRenderer { `; } - async renderVideo(path: URI, poster?: URI): Promise { + async renderVideo(path: URI, poster?: URI, description?: string): Promise { const nonce = generateUuid(); return ` @@ -218,7 +218,7 @@ export class GettingStartedDetailsRenderer { -